forked from grahampugh/multitenant-jamf-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinitialize_instance.py
More file actions
executable file
·316 lines (257 loc) · 9.43 KB
/
Copy pathinitialize_instance.py
File metadata and controls
executable file
·316 lines (257 loc) · 9.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#!/usr/bin/env python3
"""
This script initializes a new Jamf Pro instance using the Jamf Pro API.
It checks the instance status and if it requires initialization,
it sends the necessary data to initialize it.
The script accepts command line arguments for the instance URL, admin username,
password, and activation code.
It also includes error handling and logging for better debugging and user feedback.
Note: This script is designed to be run in a Python 3 environment.
It requires the requests library.
It can be installed using pip:
pip install requests
or
pip3 install requests
"""
import argparse
import json
import logging
import sys
import time
import secrets
import string
from typing import Optional
from urllib.parse import urlparse
import requests
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
class JamfInitializer:
"""
Class to handle the initialization of a Jamf Pro instance.
It checks the instance status and initializes it if required.
"""
def __init__(self, instance_url: str):
"""
Initialize the JamfInitializer with the Jamf Pro instance URL.
Args:
instance_url (str): The base URL of the Jamf Pro instance
(e.g., 'https://myjamfinstance.jamfcloud.com')
"""
self.base_url = instance_url.rstrip("/")
self.session = requests.Session()
# Disable SSL warning for self-signed certificates if needed
# requests.packages.urllib3.disable_warnings()
def check_instance_status(self) -> Optional[dict]:
"""
Check the health status of the Jamf Pro instance.
Returns:
dict: The parsed health check response or None if request fails
"""
try:
response = self.session.get(
f"{self.base_url}/api/startup-status", timeout=30
)
response.raise_for_status()
# Clean up the response - replace HTML encoded quotes
cleaned_response = response.text.replace(""", '"')
return json.loads(cleaned_response)
except requests.exceptions.RequestException as e:
logger.error("Error checking instance status: %s", str(e))
return None
except (json.JSONDecodeError, IndexError) as e:
logger.error("Error parsing health check response: %s", str(e))
return None
def initialize_instance(
self,
admin_username: str,
admin_password: str,
activation_code: str,
institution_name: str = "Jamf",
email: str = "default@example.com",
) -> bool:
"""
Initialize the Jamf Pro instance using the API.
Args:
admin_username (str): The username for the initial admin account
admin_password (str): The password for the initial admin account
activation_code (str): The activation code for the instance
institution_name (str): The name of the institution (default: "Jamf")
email (str): The email address for the initial admin account
Returns:
bool: True if initialization was successful, False otherwise
"""
try:
payload = {
"jssUrl": self.base_url,
"username": admin_username,
"password": admin_password,
"activationCode": activation_code,
"eulaAccepted": True,
"institutionName": institution_name,
"email": email,
}
# Send initialization request
response = self.session.post(
f"{self.base_url}/api/v1/system/initialize",
json=payload,
headers={"Content-Type": "application/json"},
timeout=30,
)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
logger.error("Error initializing instance: %s", str(e))
return False
def validate_url(url: str) -> str:
"""
Validate the provided URL format.
Args:
url (str): URL to validate
Returns:
str: Validated URL
Raises:
argparse.ArgumentTypeError: If URL is invalid
"""
try:
result = urlparse(url)
if all([result.scheme, result.netloc]):
return url
raise ValueError
except ValueError as exc:
raise argparse.ArgumentTypeError(
f"Invalid URL format: {url}. URL must include scheme (e.g., https://)"
) from exc
def generate_secure_password() -> str:
"""
Generate a secure password that meets Jamf Pro requirements.
Returns a 64-character password with letters, numbers, and select symbols.
Ensures at least one of each required character type is included.
"""
# Define character sets
lowercase = string.ascii_lowercase
uppercase = string.ascii_uppercase
digits = string.digits
symbols = "-_^"
# Ensure at least one of each type
password = [
secrets.choice(lowercase), # at least one lowercase
secrets.choice(uppercase), # at least one uppercase
secrets.choice(digits), # at least one number
secrets.choice(symbols), # at least one symbol
]
# Fill the rest of the password
all_chars = lowercase + uppercase + digits + symbols
password.extend(secrets.choice(all_chars) for _ in range(60)) # 64 - 4 = 60 remaining chars
# Shuffle the password to avoid predictable character positions
secrets.SystemRandom().shuffle(password)
return ''.join(password)
def parse_arguments() -> argparse.Namespace:
"""
Parse and validate command line arguments.
Returns:
argparse.Namespace: Parsed command line arguments
"""
parser = argparse.ArgumentParser(
description="Initialize a new Jamf Pro instance",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Example usage:
%(prog)s -u https://myjamfinstance.jamfcloud.com -a jamfadmin -c MyActivationCode
%(prog)s --url https://myjamfinstance.jamfcloud.com --username jamfadmin --password MySecurePassword123 -activationcode MyActivationCode
""",
)
parser.add_argument(
"-u",
"--url",
required=True,
type=validate_url,
help="Jamf Pro instance URL (e.g., https://myjamfinstance.jamfcloud.com)",
)
parser.add_argument(
"-a", "--username", required=True, help="Admin username for initialization"
)
parser.add_argument(
"-p", "--password", required=False, help="Admin password for initialization, random if not provided"
)
parser.add_argument(
"-c",
"--activationcode",
required=True,
help="Activation Code for initialization",
)
parser.add_argument(
"-i",
"--institution-name",
default="Jamf",
help="Institution name for initialization (default: Jamf)",
)
parser.add_argument(
"-e", "--email", required=False, help="Email address for initialization"
)
parser.add_argument(
"--max-attempts",
type=int,
default=10,
help="Maximum number of health check attempts (default: 10)",
)
parser.add_argument(
"--attempt-delay",
type=int,
default=30,
help="Delay in seconds between attempts (default: 30)",
)
return parser.parse_args()
def main():
"""
Main function to run the script.
Parses command line arguments, initializes the Jamf instance,
and handles the initialization process.
"""
# Parse command line arguments
args = parse_arguments()
if not args.password:
generated_password = generate_secure_password()
args.password = generated_password
logger.info("Generated secure random password:")
print(f"\nGenerated Password: {generated_password}\n")
logger.info("Please save this password in a secure location!")
initializer = JamfInitializer(args.url)
logger.info("Starting initialization process for %s", args.url)
for attempt in range(args.max_attempts):
logger.info(
"Checking instance status (attempt %d/%d)", attempt + 1, args.max_attempts
)
status = initializer.check_instance_status()
if status is None:
logger.warning("Unable to get instance status, will retry...")
time.sleep(args.attempt_delay)
continue
logger.info("Health check status: %s", status)
if status.get("setupAssistantNecessary") is True:
logger.info("Instance requires initialization, proceeding...")
if initializer.initialize_instance(
args.username,
args.password,
args.activationcode,
args.institution_name,
args.email,
):
logger.info("Instance initialization successful!")
sys.exit(0)
else:
logger.error("Instance initialization failed!")
sys.exit(1)
else:
logger.info("Instance is already initialized or in an unexpected state")
sys.exit(0)
time.sleep(args.attempt_delay)
logger.error(
"Maximum attempts (%d) reached without successful initialization",
args.max_attempts,
)
sys.exit(1)
if __name__ == "__main__":
main()