Skip to content

Commit 320f376

Browse files
authored
Merge pull request #131 from release-engineering/azure-preview-to-live-fix
Azure: fix bug preventing pushing preview offers live
2 parents ec1d3cd + 7971816 commit 320f376

File tree

7 files changed

+296
-84
lines changed

7 files changed

+296
-84
lines changed

cloudpub/ms_azure/service.py

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ def _configure(self, data: Dict[str, Any]) -> ConfigureStatus:
103103
Returns:
104104
The job ID to track its status alongside the initial status.
105105
"""
106-
log.debug("Received the following data to create/modify: %s" % json.dumps(data, indent=2))
106+
if log.isEnabledFor(logging.DEBUG):
107+
log.debug(
108+
"Received the following data to create/modify: %s", json.dumps(data, indent=2)
109+
)
107110
resp = self.session.post(path="configure", json=data)
108111
self._raise_for_status(response=resp)
109112
rsp_data = resp.json()
@@ -121,17 +124,19 @@ def _query_job_details(self, job_id: str) -> ConfigureStatus:
121124
Returns:
122125
The updated job status.
123126
"""
124-
log.debug(f"Query job details for \"{job_id}\"")
127+
log.debug("Query job details for \"%s\"", job_id)
125128
resp = self.session.get(path=f"configure/{job_id}/status")
126129

127130
# We don't want to fail if there's a server error thus we make a fake
128131
# response for it so the query job details will be retried.
129132
if resp.status_code >= 500:
130133
log.warning(
131134
(
132-
f"Got HTTP {resp.status_code} from server when querying job {job_id} status."
133-
" Considering the job_status as \"pending\"."
134-
)
135+
"Got HTTP %s from server when querying job %s status."
136+
" Considering the job_status as \"pending\".",
137+
),
138+
resp.status_code,
139+
job_id,
135140
)
136141
return ConfigureStatus.from_json(
137142
{
@@ -177,7 +182,7 @@ def _wait_for_job_completion(self, job_id: str) -> ConfigureStatus:
177182
error_message = f"Job {job_id} failed: \n{job_details.errors}"
178183
self._raise_error(InvalidStateError, error_message)
179184
elif job_details.job_result == "succeeded":
180-
log.debug(f"Job {job_id} succeeded")
185+
log.debug("Job %s succeeded", job_id)
181186
return job_details
182187

183188
def configure(self, resource: AzureResource) -> ConfigureStatus:
@@ -194,7 +199,8 @@ def configure(self, resource: AzureResource) -> ConfigureStatus:
194199
"$schema": self.CONFIGURE_SCHEMA.format(AZURE_API_VERSION=self.AZURE_API_VERSION),
195200
"resources": [resource.to_json()],
196201
}
197-
log.info("Data to configure: %s", json.dumps(data, indent=2))
202+
if log.isEnabledFor(logging.DEBUG):
203+
log.debug("Data to configure: %s", json.dumps(data, indent=2))
198204
res = self._configure(data=data)
199205
return self._wait_for_job_completion(job_id=res.job_id)
200206

@@ -205,7 +211,7 @@ def products(self) -> Iterator[ProductSummary]:
205211
params: Dict[str, str] = {}
206212

207213
while has_next:
208-
log.debug("Requesting the products list.")
214+
log.info("Requesting the products list.")
209215
resp = self.session.get(path="/product", params=params)
210216
data = self._assert_dict(resp)
211217

@@ -230,6 +236,7 @@ def list_products(self) -> List[ProductSummary]:
230236
Returns:
231237
list: A list with ProductSummary for all products in Azure.
232238
"""
239+
log.info("Listing the products on Azure server.")
233240
if not self._products:
234241
self._products = [p for p in self.products]
235242
return self._products
@@ -257,7 +264,7 @@ def get_product(self, product_id: str, first_target: str = "preview") -> Product
257264
targets.append(tgt)
258265

259266
for t in targets:
260-
log.debug("Requesting the product ID \"%s\" with state \"%s\".", product_id, t)
267+
log.info("Requesting the product ID \"%s\" with state \"%s\".", product_id, t)
261268
try:
262269
resp = self.session.get(
263270
path=f"/resource-tree/product/{product_id}", params={"targetType": t}
@@ -314,6 +321,7 @@ def get_submission_state(self, product_id, state="preview") -> Optional[ProductS
314321
Returns:
315322
Optional[ProductSubmission]: The requested submission when found.
316323
"""
324+
log.info("Looking up for submission in state \"%s\" for \"%s\"", state, product_id)
317325
submissions = self.get_submissions(product_id)
318326
for sub in submissions:
319327
if sub.target.targetType == state:
@@ -370,15 +378,15 @@ def get_plan_by_name(self, product: Product, plan_name: str) -> PlanSummary:
370378

371379
def get_product_plan_by_name(
372380
self, product_name: str, plan_name: str
373-
) -> Tuple[Product, PlanSummary]:
381+
) -> Tuple[Product, PlanSummary, str]:
374382
"""Return a tuple with the desired Product and Plan after iterating over all targets.
375383
376384
Args:
377385
product_name (str): The name of the product to search for
378386
plan_name (str): The name of the plan to search for
379387
380388
Returns:
381-
Tuple[Product, PlanSummary]: The Product and PlanSummary when fonud
389+
Tuple[Product, PlanSummary, str]: The Product, PlanSummary and target when fonud
382390
Raises:
383391
NotFoundError whenever all targets are exhausted and no information was found
384392
"""
@@ -388,7 +396,7 @@ def get_product_plan_by_name(
388396
try:
389397
product = self.get_product_by_name(product_name, first_target=tgt)
390398
plan = self.get_plan_by_name(product, plan_name)
391-
return product, plan
399+
return product, plan, tgt
392400
except NotFoundError:
393401
continue
394402
self._raise_error(
@@ -421,6 +429,7 @@ def submit_to_status(self, product_id: str, status: str) -> ConfigureStatus:
421429
Returns:
422430
The response from configure request.
423431
"""
432+
log.info("Submitting the status of \"%s\" to \"%s\"", product_id, status)
424433
# We need to get the previous state of the given one to request the submission
425434
prev_state_mapping = {
426435
"preview": "draft",
@@ -459,6 +468,7 @@ def ensure_can_publish(self, product_id: str) -> None:
459468
Raises:
460469
RuntimeError: whenever a publishing is already in progress.
461470
"""
471+
log.info("Ensuring no other publishing jobs are in progress for \"%s\"", product_id)
462472
submission_targets = ["preview", "live"]
463473

464474
for target in submission_targets:
@@ -539,21 +549,21 @@ def _publish_preview(self, product: Product, product_name: str) -> None:
539549
List[ProductSubmission],
540550
self.filter_product_resources(product=product, resource="submission"),
541551
)[0]
542-
if not self._is_submission_in_preview(submission):
543-
log.info(
544-
"Submitting the product \"%s (%s)\" to \"preview\"." % (product_name, product.id)
552+
if self._is_submission_in_preview(submission):
553+
log.info("The product \"%s\" is already set to preview", product_name)
554+
return
555+
556+
res = self.submit_to_status(product_id=product.id, status='preview')
557+
558+
if res.job_result != 'succeeded' or not self.get_submission_state(
559+
product.id, state="preview"
560+
):
561+
errors = "\n".join(res.errors)
562+
failure_msg = (
563+
f"Failed to submit the product {product.id} to preview. "
564+
f"Status: {res.job_result} Errors: {errors}"
545565
)
546-
res = self.submit_to_status(product_id=product.id, status='preview')
547-
548-
if res.job_result != 'succeeded' or not self.get_submission_state(
549-
product.id, state="preview"
550-
):
551-
errors = "\n".join(res.errors)
552-
failure_msg = (
553-
f"Failed to submit the product {product.id} to preview. "
554-
f"Status: {res.job_result} Errors: {errors}"
555-
)
556-
raise RuntimeError(failure_msg)
566+
raise RuntimeError(failure_msg)
557567

558568
@retry(
559569
wait=wait_fixed(wait=60),
@@ -572,7 +582,6 @@ def _publish_live(self, product: Product, product_name: str) -> None:
572582
"""
573583
# Note: the offer can only go `live` after successfully being changed to `preview`
574584
# which takes up to 4 days.
575-
log.info("Submitting the product \"%s (%s)\" to \"live\"." % (product_name, product.id))
576585
res = self.submit_to_status(product_id=product.id, status='live')
577586

578587
if res.job_result != 'succeeded' or not self.get_submission_state(product.id, state="live"):
@@ -596,71 +605,111 @@ def publish(self, metadata: AzurePublishingMetadata) -> None:
596605
# "product-name/plan-name"
597606
product_name = metadata.destination.split("/")[0]
598607
plan_name = metadata.destination.split("/")[-1]
599-
product, plan = self.get_product_plan_by_name(product_name, plan_name)
608+
product, plan, tgt = self.get_product_plan_by_name(product_name, plan_name)
600609
log.info(
601-
"Preparing to associate the image with the plan \"%s\" from product \"%s\""
602-
% (plan_name, product_name)
610+
"Preparing to associate the image \"%s\" with the plan \"%s\" from product \"%s\" on \"%s\"", # noqa: E501
611+
metadata.image_path,
612+
plan_name,
613+
product_name,
614+
tgt,
603615
)
604616

605617
# 2. Retrieve the VM Technical configuration for the given plan
606-
log.debug("Retrieving the technical config for \"%s\"." % metadata.destination)
618+
log.info("Retrieving the technical config for \"%s\" on \"%s\".", metadata.destination, tgt)
607619
tech_config = self.get_plan_tech_config(product, plan)
608620

609621
# 3. Prepare the Disk Version
610-
log.debug("Creating the VMImageResource with SAS: \"%s\"" % metadata.image_path)
622+
log.info("Creating the VMImageResource with SAS for image: \"%s\"", metadata.image_path)
611623
sas = OSDiskURI(uri=metadata.image_path)
612624
source = VMImageSource(source_type="sasUri", os_disk=sas.to_json(), data_disks=[])
613625

614626
# Note: If `overwrite` is True it means we can set this VM image as the only one in the
615627
# plan's technical config and discard all other VM images which may've been present.
616628
disk_version = None # just to make mypy happy
617629
if metadata.overwrite is True:
618-
log.warning("Overwriting the plan %s with the given image.", plan_name)
630+
log.warning(
631+
"Overwriting the plan \"%s\" on \"%s\" with the given image: \"%s\".",
632+
plan_name,
633+
tgt,
634+
metadata.image_path,
635+
)
619636
disk_version = create_disk_version_from_scratch(metadata, source)
620637
tech_config.disk_versions = [disk_version]
621638

622639
# We just want to append a new image if the SAS is not already present.
623640
elif not is_sas_present(tech_config, metadata.image_path, metadata.check_base_sas_only):
624641
# Here we can have the metadata.disk_version set or empty.
625642
# When set we want to get the existing disk_version which matches its value.
626-
log.debug("Scanning the disk versions from %s" % metadata.destination)
643+
log.info(
644+
"Scanning the disk versions from \"%s\" on \"%s\" for the image \"%s\"",
645+
metadata.destination,
646+
tgt,
647+
metadata.image_path,
648+
)
627649
disk_version = seek_disk_version(tech_config, metadata.disk_version)
628650

629651
# Check the images of the selected DiskVersion if it exists
630652
if disk_version:
631-
log.debug(
632-
"DiskVersion \"%s\" exists in \"%s\"."
633-
% (disk_version.version_number, metadata.destination)
653+
log.info(
654+
"DiskVersion \"%s\" exists in \"%s\" on \"%s\" for the image \"%s\".",
655+
disk_version.version_number,
656+
metadata.destination,
657+
tgt,
658+
metadata.image_path,
634659
)
635660
disk_version = set_new_sas_disk_version(disk_version, metadata, source)
636661

637662
else: # The disk version doesn't exist, we need to create one from scratch
638-
log.debug("The DiskVersion doesn't exist, creating one from scratch.")
663+
log.info("The DiskVersion doesn't exist, creating one from scratch.")
639664
disk_version = create_disk_version_from_scratch(metadata, source)
640665
tech_config.disk_versions.append(disk_version)
641666
else:
642667
log.info(
643-
"The destination \"%s\" already contains the SAS URI: \"%s\"."
644-
% (metadata.destination, metadata.image_path)
668+
"The destination \"%s\" on \"%s\" already contains the SAS URI: \"%s\".",
669+
metadata.destination,
670+
tgt,
671+
metadata.image_path,
645672
)
646673

647674
# 4. With the updated disk_version we should adjust the SKUs and submit the changes
648675
if disk_version:
649-
log.debug("Updating SKUs for \"%s\"." % metadata.destination)
676+
log.info("Updating SKUs for \"%s\" on \"%s\".", metadata.destination, tgt)
650677
tech_config.skus = update_skus(
651678
disk_versions=tech_config.disk_versions,
652679
generation=metadata.generation,
653680
plan_name=plan_name,
654681
old_skus=tech_config.skus,
655682
)
656-
log.debug("Updating the technical configuration for \"%s\"." % metadata.destination)
683+
log.info(
684+
"Updating the technical configuration for \"%s\" on \"%s\".",
685+
metadata.destination,
686+
tgt,
687+
)
657688
self.configure(resource=tech_config)
658689

659690
# 5. Proceed to publishing if it was requested.
660691
# Note: The publishing will only occur if it made changes in disk_version.
661-
if disk_version and not metadata.keepdraft:
662-
logdiff(self.diff_offer(product))
663-
self.ensure_can_publish(product.id)
692+
if not metadata.keepdraft:
693+
# Get the submission state
694+
submission: ProductSubmission = cast(
695+
List[ProductSubmission],
696+
self.filter_product_resources(product=product, resource="submission"),
697+
)[0]
698+
699+
# We should only publish if there are new changes OR
700+
# the existing offer was already in preview
701+
if disk_version or self._is_submission_in_preview(submission):
702+
log.info(
703+
"Publishing the new changes for \"%s\" on plan \"%s\"", product_name, plan_name
704+
)
705+
logdiff(self.diff_offer(product))
706+
self.ensure_can_publish(product.id)
707+
708+
self._publish_preview(product, product_name)
709+
self._publish_live(product, product_name)
664710

665-
self._publish_preview(product, product_name)
666-
self._publish_live(product, product_name)
711+
log.info(
712+
"Finished publishing the image \"%s\" to \"%s\"",
713+
metadata.image_path,
714+
metadata.destination,
715+
)

cloudpub/ms_azure/session.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self, json: Dict[str, str]):
2424
"""
2525
self.expires_on = datetime.fromtimestamp(int(json["expires_on"]))
2626
self.access_token = json["access_token"]
27-
log.debug(f"Obtained token with expiration date on {self.expires_on}")
27+
log.debug("Obtained token with expiration date on %s", self.expires_on)
2828

2929
def is_expired(self) -> bool:
3030
"""Return True if the token is expired and False otherwise."""
@@ -108,7 +108,7 @@ def _validate_auth_keys(auth_keys: Dict[str, str]) -> Dict[str, str]:
108108
"AZURE_API_SECRET",
109109
]
110110
for key in mandatory_keys:
111-
log.debug(f"Validating mandatory key \"{key}\"")
111+
log.debug("Validating mandatory key \"%s\"", key)
112112
if key not in auth_keys.keys() or not auth_keys.get(key):
113113
err_msg = f'The key/value for "{key}" must be set.'
114114
log.error(err_msg)
@@ -117,7 +117,7 @@ def _validate_auth_keys(auth_keys: Dict[str, str]) -> Dict[str, str]:
117117

118118
def _login(self) -> AccessToken:
119119
"""Retrieve the authentication token from Microsoft."""
120-
log.info("Retrieving the bearer token from Microsoft")
120+
log.debug("Retrieving the bearer token from Microsoft")
121121
url = self.LOGIN_URL_TMPL.format(**self.auth_keys)
122122

123123
headers = {
@@ -156,7 +156,7 @@ def _request(
156156
params = {}
157157
params.update(self._mandatory_params)
158158

159-
log.info(f"Sending a {method} request to {path}")
159+
log.debug("Sending a %s request to %s", method, path)
160160
formatted_url = self._prefix_url.format(**self.auth_keys)
161161
url = join_url(formatted_url, path)
162162
return self.session.request(method, url=url, params=params, headers=headers, **kwargs)

0 commit comments

Comments
 (0)