Skip to content

Commit

Permalink
api: add u.pro.services.enable.v1
Browse files Browse the repository at this point in the history
  • Loading branch information
orndorffgrant committed Jan 4, 2024
1 parent ff75677 commit b5f5e7c
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 13 deletions.
169 changes: 169 additions & 0 deletions features/api_enable.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
Feature: u.pro.services.enable

Scenario Outline: u.pro.services.enable.v1 container services
Given a `<release>` `<machine_type>` machine with ubuntu-advantage-tools installed
When I run `apt-get update` with sudo
And I apt install `jq`
And I attach `contract_token` with sudo and options `--no-auto-enable`

# Basic enable
And I run shell command `pro api u.pro.services.enable.v1 --args service=esm-infra | jq .data.attributes` with sudo
Then I will see the following on stdout:
"""
{
"disabled": [],
"enabled": [
"esm-infra"
],
"messages": [],
"reboot_required": false
}
"""
When I run shell command `pro api u.pro.status.enabled_services.v1 | jq .data.attributes.enabled_services ` with sudo
Then stdout contains substring:
"""
esm-infra
"""
# Enable already enabled service succeeds
When I run shell command `pro api u.pro.services.enable.v1 --args service=esm-infra | jq .data.attributes` with sudo
Then I will see the following on stdout:
"""
{
"disabled": [],
"enabled": [],
"messages": [],
"reboot_required": false
}
"""
# Disallowing required services when required causes error
When I verify that running `pro api u.pro.services.enable.v1 --data '{"service": "ros", "enable_required_services": false}'` `with sudo` exits `1`
When I run shell command `pro api u.pro.services.enable.v1 --data \"{\\\"service\\\": \\\"ros\\\", \\\"enable_required_services\\\": false}\" | jq .errors[0]` with sudo
Then I will see the following on stdout:
"""
{
"code": "enable-blocked-required-service",
"meta": {
"required": "esm-apps",
"target": "ros"
},
"title": "Could not enable ros because esm-apps is not enabled"
}
"""
# Default allows enabling required services
When I run shell command `pro api u.pro.services.enable.v1 --args service=ros | jq .data.attributes` with sudo
Then I will see the following on stdout:
"""
{
"disabled": [],
"enabled": [
"esm-apps",
"ros"
],
"messages": [],
"reboot_required": false
}
"""
# Access only works and post enable messages work
When I run shell command `pro api u.pro.services.enable.v1 --data \"{\\\"service\\\": \\\"cis\\\", \\\"access_only\\\": true}\" | jq .data.attributes` with sudo
Then I will see the following on stdout:
"""
{
"disabled": [],
"enabled": [
"cis"
],
"messages": [
"Visit https://ubuntu.com/security/cis to learn how to use CIS"
],
"reboot_required": false
}
"""
When I run `apt-cache policy usg-common` as non-root
Then stdout contains substring:
"""
Installed: (none)
"""
# Access only on service that doesn't support it fails
When I verify that running `pro api u.pro.services.enable.v1 --data '{"service": "ros-updates", "access_only": true}'` `with sudo` exits `1`
When I run shell command `pro api u.pro.services.enable.v1 --data \"{\\\"service\\\": \\\"ros-updates\\\", \\\"access_only\\\": true}\" | jq .errors[0]` with sudo
Then I will see the following on stdout:
"""
{
"code": "entitlement-not-enabled",
"meta": {
"reason": {
"code": "enable-access-only-not-supported",
"title": "ROS ESM All Updates does not support being enabled with --access-only"
}
},
"title": "failed to enable ros-updates"
}
"""
Examples:
| release | machine_type |
| xenial | lxd-container |
| bionic | lxd-container |
| focal | lxd-container |
| jammy | lxd-container |
Scenario Outline: u.pro.services.enable.v1 vm services
Given a `<release>` `<machine_type>` machine with ubuntu-advantage-tools installed
When I run `apt-get update` with sudo
And I apt install `jq`
And I attach `contract_token` with sudo and options `--no-auto-enable`
# Basic enable
And I run shell command `pro api u.pro.services.enable.v1 --args service=livepatch | jq .data.attributes` with sudo
Then I will see the following on stdout:
"""
{
"disabled": [],
"enabled": [
"livepatch"
],
"messages": [],
"reboot_required": false
}
"""
# Enable without disable_incompatible fails
When I verify that running `pro api u.pro.services.enable.v1 --data '{"service": "realtime-kernel", "disable_incompatible_services": false}'` `with sudo` exits `1`
When I run shell command `pro api u.pro.services.enable.v1 --data \"{\\\"service\\\": \\\"realtime-kernel\\\", \\\"disable_incompatible_services\\\": false}\" | jq .errors[0]` with sudo
Then I will see the following on stdout:
"""
{
"code": "enable-blocked-incompatible-service",
"meta": {
"incompatible": "livepatch",
"target": "realtime-kernel"
},
"title": "Could not enable realtime-kernel because livepatch is enabled"
}
"""
# Enable with disable_incompatible works and variant works
When I run shell command `pro api u.pro.services.enable.v1 --data \"{\\\"service\\\": \\\"realtime-kernel\\\", \\\"variant\\\": "intel-iotg"}\" | jq .data.attributes` with sudo
Then I will see the following on stdout:
"""
{
"disabled": [
"livepatch"
],
"enabled": [
"realtime-kernel"
],
"messages": [],
"reboot_required": false
}
"""
When I run shell command `pro api u.pro.status.enabled_services.v1 | jq ".data.attributes.enabled_services | select(.name==\"realtime-kernel\")" ` with sudo
Then stdout contains substring:
"""
{
"name": "realtime-kernel",
"variant_enabled": true,
"variant_name": "intel-iotg"
}
"""
Examples:
| release | machine_type |
| jammy | lxd-vm |
1 change: 1 addition & 0 deletions uaclient/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"u.pro.security.fix.usn.plan.v1",
"u.pro.security.status.livepatch_cves.v1",
"u.pro.security.status.reboot_required.v1",
"u.pro.services.enable.v1",
"u.pro.status.enabled_services.v1",
"u.pro.status.is_attached.v1",
"u.pro.version.v1",
Expand Down
8 changes: 8 additions & 0 deletions uaclient/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
AlreadyAttachedError,
ConnectivityError,
ContractAPIError,
EnableBlockedByIncompatibleService,
EnableBlockedByRequiredService,
EntitlementNotEnabledError,
EntitlementNotFoundError,
EntitlementsNotEnabledError,
InvalidProImage,
LockHeldError,
NonAutoAttachImageError,
UbuntuProError,
UnattachedError,
UrlError,
UserFacingError,
)
Expand All @@ -23,9 +27,13 @@
"LockHeldError",
"NonAutoAttachImageError",
"UbuntuProError",
"UnattachedError",
"UrlError",
"UserFacingError",
"EntitlementsNotEnabledError",
"EntitlementNotEnabledError",
"EnableBlockedByIncompatibleService",
"EnableBlockedByRequiredService",
]


Expand Down
Empty file.
Empty file.
167 changes: 167 additions & 0 deletions uaclient/api/u/pro/services/enable/v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import logging
from typing import List, Optional

from uaclient import actions, entitlements, event_logger, lock, messages, util
from uaclient.api import exceptions
from uaclient.api.api import APIEndpoint
from uaclient.api.data_types import AdditionalInfo
from uaclient.api.u.pro.status.enabled_services.v1 import _enabled_services
from uaclient.api.u.pro.status.is_attached.v1 import _is_attached
from uaclient.config import UAConfig
from uaclient.data_types import (
BoolDataValue,
DataObject,
Field,
StringDataValue,
data_list,
)

event = event_logger.get_event_logger()
LOG = logging.getLogger(util.replace_top_level_logger_name(__name__))


class EnableOptions(DataObject):
fields = [
Field("service", StringDataValue),
Field("variant", StringDataValue, False),
Field("enable_required_services", BoolDataValue, False),
Field("disable_incompatible_services", BoolDataValue, False),
Field("access_only", BoolDataValue, False),
]

def __init__(
self,
*,
service: str,
variant: Optional[str] = None,
enable_required_services: bool = True,
disable_incompatible_services: bool = True,
access_only: bool = False
):
self.service = service
self.variant = variant
self.enable_required_services = enable_required_services
self.disable_incompatible_services = disable_incompatible_services
self.access_only = access_only


class EnableResult(DataObject, AdditionalInfo):
fields = [
Field("enabled", data_list(StringDataValue)),
Field("disabled", data_list(StringDataValue)),
Field("reboot_required", BoolDataValue),
Field("messages", data_list(StringDataValue)),
]

def __init__(
self,
*,
enabled: List[str],
disabled: List[str],
reboot_required: bool,
messages: List[str]
):
self.enabled = enabled
self.disabled = disabled
self.reboot_required = reboot_required
self.messages = messages


def _enabled_services_names(cfg: UAConfig) -> List[str]:
return [s.name for s in _enabled_services(cfg).enabled_services]


def enable(options: EnableOptions) -> EnableResult:
return _enable(options, UAConfig())


def _enable(
options: EnableOptions,
cfg: UAConfig,
) -> EnableResult:
event.set_event_mode(event_logger.EventLoggerMode.JSON)

if not _is_attached(cfg).is_attached:
raise exceptions.UnattachedError()

enabled_services_before = _enabled_services_names(cfg)
if options.service in enabled_services_before:
# nothing to do
return EnableResult(
enabled=[],
disabled=[],
reboot_required=False,
messages=[],
)

ent_cls = entitlements.entitlement_factory(
cfg=cfg, name=options.service, variant=options.variant or ""
)
entitlement = ent_cls(
cfg,
assume_yes=True,
allow_beta=True,
called_name=options.service,
access_only=options.access_only,
)

success = False
fail_reason = None

try:
with lock.SpinLock(
cfg=cfg,
lock_holder="u.pro.services.enable.v1",
):
success, fail_reason = entitlement.enable(
enable_required_services=options.enable_required_services,
disable_incompatible_services=options.disable_incompatible_services,
api=True,
)
except Exception as e:
lock.clear_lock_file_if_present()
raise e

if not success:
if fail_reason is not None:
reason = fail_reason.message
else:
reason = messages.GENERIC_UNKNOWN_ISSUE
raise exceptions.EntitlementNotEnabledError(
service=options.service, reason=reason
)

enabled_services_after = _enabled_services_names(cfg)

post_enable_messages = [
msg
for msg in entitlement.messaging.get("post_enable", [])
if isinstance(msg, str)
]

return EnableResult(
enabled=sorted(
list(
set(enabled_services_after).difference(
set(enabled_services_before)
)
)
),
disabled=sorted(
list(
set(enabled_services_before).difference(
set(enabled_services_after)
)
)
),
reboot_required=entitlement._check_for_reboot(),
messages=post_enable_messages,
)


endpoint = APIEndpoint(
version="v1",
name="EnableService",
fn=_enable,
options_cls=EnableOptions,
)
Loading

0 comments on commit b5f5e7c

Please sign in to comment.