Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VERY DRAFT] Dima's hacks #1097

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ clean:

.PHONY: client
client:
tox -r --notest -e lint,py3
# FIXME temporarily commented out
# tox -r --notest -e lint,py3
# why abuse tox venv this way?
$(PY) -m juju.client.facade -s "juju/client/schemas*" -o juju/client/

.PHONY: run-unit-tests
Expand Down
2 changes: 1 addition & 1 deletion docs/readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Documentation: https://pythonlibjuju.readthedocs.io/en/latest/
Requirements
------------

* Python 3.9/3.10
* Python 3.11


Design Notes
Expand Down
5 changes: 3 additions & 2 deletions examples/formatted_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
description. For a similar solution using the FullStatus object
check examples/fullstatus.py
"""
import asyncio
from juju import jasyncio
import logging
import sys
Expand All @@ -30,7 +31,7 @@ async def main():
channel='stable',
)

await jasyncio.sleep(10)
await asyncio.sleep(10)
tmp = tempfile.NamedTemporaryFile(delete=False)
LOG.info('status dumped to %s', tmp.name)
with open(tmp.name, 'w') as f:
Expand All @@ -40,7 +41,7 @@ async def main():
# await formatted_status(model, target=sys.stdout)
await formatted_status(model, target=f)
f.write('-----------\n')
await jasyncio.sleep(1)
await asyncio.sleep(1)
await application.remove()
await model.disconnect()

Expand Down
5 changes: 3 additions & 2 deletions examples/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
This example demonstrate how status works

"""
import asyncio
from juju import jasyncio
import logging
import sys
Expand All @@ -26,7 +27,7 @@ async def main():
series='jammy',
channel='stable',
)
await jasyncio.sleep(10)
await asyncio.sleep(10)
# Print the status to observe the evolution
# during a minute
for i in range(12):
Expand All @@ -39,7 +40,7 @@ async def main():
print(status)
except Exception as e:
print(e)
await jasyncio.sleep(5)
await asyncio.sleep(5)

print('Removing ubuntu')
await application.remove()
Expand Down
158 changes: 143 additions & 15 deletions juju/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import hashlib
import json
import logging
import typing
from pathlib import Path
import pathlib
import warnings
from typing import Any, List, TYPE_CHECKING

import juju.client.facade
from . import jasyncio, model, tag, utils
from .annotationhelper import _get_annotations, _set_annotations
from .bundle import get_charm_series, is_local_charm
from .client import client
from .client import client, _definitions
from .errors import JujuApplicationConfigError, JujuError
from .origin import Channel
from .placement import parse as parse_placement
Expand All @@ -20,10 +22,96 @@
from .utils import block_until
from .version import DEFAULT_ARCHITECTURE

if TYPE_CHECKING:
from .unit import Unit

log = logging.getLogger(__name__)

"""
# juju:rpc/params/multiwatcher.go

// StatusInfo holds the unit and machine status information. It is
// used by ApplicationInfo and UnitInfo.
type StatusInfo struct {
Err error `json:"err,omitempty"`
Current status.Status `json:"current"`
Message string `json:"message"`
Since *time.Time `json:"since,omitempty"`
Version string `json:"version"`
Data map[string]interface{} `json:"data,omitempty"`
}

// ApplicationInfo holds the information about an application that is tracked
// by multiwatcherStore.
type ApplicationInfo struct {
ModelUUID string `json:"model-uuid"`
Name string `json:"name"`
Exposed bool `json:"exposed"`
CharmURL string `json:"charm-url"`
OwnerTag string `json:"owner-tag"`
Life life.Value `json:"life"`
MinUnits int `json:"min-units"`
Constraints constraints.Value `json:"constraints"`
Config map[string]interface{} `json:"config,omitempty"`
Subordinate bool `json:"subordinate"`
Status StatusInfo `json:"status"`
WorkloadVersion string `json:"workload-version"`
}
"""

_FIXME = object()

class Application(model.ModelEntity):
# What safe data usually contains
_expected_attributes = [
"model_uuid", # FIXME is it even useful? There's always a link to the model.
"name",
"exposed",
"charm_url",
"owner_tag",
"life",
"min_units",
"constraints",
"subordinate",
"status",
"workload_version",
]

model_uuid: str
name: str
exposed: bool # present on ApplicationResult, ApplicationStatus
charm_url: str
# owner_tag: str # This is weird, present on Model, Secret, Storage, ApplicationOffer, MigrationModel; not app
life: Any # Life, present on ApplicationStatus
# min_units: int
constraints: _definitions.Value
config: dict[str, Any] # json-able # FIXME may be omitted
subordinate: bool
# status: Any # Status # @property
# workload_version: str # e.g. ApplicationStatus, maybe EntityXxx

_pk: str|int

def _facade_to_data(self, obj: juju.client.facade.Type) -> dict:
value = obj.serialize()
rv = {
"name": value.pop("application"),
"exposed": _FIXME,
"charm_url": value.pop("charm"),
"owner_tag": _FIXME,
"life": _FIXME,
"min_units": _FIXME,
"constraints": _definitions.Value.from_json(value.pop("constraints")),
"subordinate": _FIXME,
"status": _FIXME, # needs a separate API call
"workload_version": _FIXME,
}
__import__("pdb").set_trace()
if value:
logging.info("Unused Application.Get fields %s", list(value))
logging.debug("Unused Application.Get data %s", value)
return rv

@property
def _unit_match_pattern(self):
return r'^{}.*$'.format(self.entity_id)
Expand All @@ -34,6 +122,9 @@ def _facade(self):
def _facade_version(self):
return client.ApplicationFacade.best_facade_version(self.connection)

def _sync_facade(self) -> juju.client.facade.Type:
return client.ApplicationFacade.from_sync_connection(self.model.sync_connection())

def on_unit_add(self, callable_):
"""Add a "unit added" observer to this entity, which will be called
whenever a unit is added to this application.
Expand All @@ -51,7 +142,34 @@ def on_unit_remove(self, callable_):
callable_, 'unit', 'remove', self._unit_match_pattern)

@property
def units(self):
def min_units(self) -> int:
warnings.warn(
"`Application.min_units` is deprecated and will soon be removed",
DeprecationWarning,
stacklevel=2,
)
return self.__getattr__("min_units")

@property
def owner_tag(self) -> str:
warnings.warn(
"`Application.owner_tag` is deprecated and will soon be removed",
DeprecationWarning,
stacklevel=2,
)
return self.__getattr__("owner_tag")

@property
def workload_version(self) -> str:
warnings.warn(
"`Application.workload_version` is deprecated, use Unit.workload_version instead",
DeprecationWarning,
stacklevel=2,
)
return self.__getattr__("workload_version")

@property
def units(self) -> list[Unit]:
return [
unit for unit in self.model.units.values()
if unit.application == self.name
Expand All @@ -63,7 +181,7 @@ def subordinate_units(self):
return [u for u in self.units if u.is_subordinate]

@property
def relations(self) -> typing.List[Relation]:
def relations(self) -> List[Relation]:
return [rel for rel in self.model.relations if rel.matches(self.name)]

def related_applications(self, endpoint_name=None):
Expand All @@ -87,6 +205,9 @@ def status(self):
If the application is unknown it will attempt to derive the unit
workload status and highlight the most relevant (severity).
"""
# FIXME how to undo this mess?
# users rely on status subscript
# users may rely on status inference from units
status = self.safe_data['status']['current']
if status == "unset":
known_statuses = []
Expand Down Expand Up @@ -578,7 +699,7 @@ def charm_name(self):
return URL.parse(self.charm_url).name

@property
def charm_url(self):
def __fixme_remove_charm_url(self):
"""Get the charm url for this application

:return str: The charm url
Expand Down Expand Up @@ -659,7 +780,7 @@ async def set_constraints(self, constraints):

async def refresh(
self, channel=None, force=False, force_series=False, force_units=False,
path=None, resources=None, revision=None, switch=None):
path: str|None =None, resources=None, revision=None, switch: str|None =None):
"""Refresh the charm for this application.

:param str channel: Channel to use when getting the charm from the
Expand Down Expand Up @@ -695,8 +816,9 @@ async def refresh(

current_origin = charm_url_origin_result.charm_origin
if path is not None or (switch is not None and is_local_charm(switch)):
await self.local_refresh(current_origin, force, force_series,
force_units, path or switch, resources)
charm_path: str = path or switch # type: ignore # validated above
await self.local_refresh(charm_origin=current_origin, force=force, force_series=force_series,
force_units=force_units, path=charm_path, resources=resources)
return

origin = _refresh_origin(current_origin, channel, revision)
Expand Down Expand Up @@ -782,7 +904,7 @@ async def refresh(
revision=_arg_res_revisions.get(res_name, -1),
type_=resource.get('Type', resource.get('type')),
origin='store',
))
)) # type: ignore # FIXME later

response = await resources_facade.AddPendingResources(
application_tag=self.tag,
Expand Down Expand Up @@ -822,9 +944,15 @@ async def refresh(
upgrade_charm = refresh

async def local_refresh(
self, charm_origin=None, force=False, force_series=False,
self,
*,
charm_origin: _definitions.CharmOrigin,
force=False,
force_series=False,
force_units=False,
path=None, resources=None):
path: str,
resources=None,
):
"""Refresh the charm for this application with a local charm.

:param dict charm_origin: The charm origin of the destination charm
Expand All @@ -842,8 +970,8 @@ async def local_refresh(

if isinstance(path, str) and path.startswith("local:"):
path = path[6:]
path = Path(path)
charm_dir = path.expanduser().resolve()
charm_path = pathlib.Path(path)
charm_dir = charm_path.expanduser().resolve()
model_config = await self.get_config()

series = (
Expand All @@ -859,7 +987,7 @@ async def local_refresh(
if default_series:
series = default_series.value
charm_url = await self.model.add_local_charm_dir(charm_dir, series)
metadata = utils.get_local_charm_metadata(path)
metadata = utils.get_local_charm_metadata(charm_path)
if resources is not None:
resources = await self.model.add_local_resources(self.entity_id,
charm_url,
Expand Down
3 changes: 2 additions & 1 deletion juju/charmhub.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2023 Canonical Ltd.
# Licensed under the Apache V2, see LICENCE file for details.

import asyncio
from .client import client
from .errors import JujuError
from juju import jasyncio
Expand All @@ -22,7 +23,7 @@ async def request_charmhub_with_retry(self, url, retries):
_response = requests.get(url)
if _response.status_code == 200:
return _response
await jasyncio.sleep(5)
await asyncio.sleep(5)
raise JujuError("Got {} from {}".format(_response.status_code, url))

async def get_charm_id(self, charm_name):
Expand Down
23 changes: 21 additions & 2 deletions juju/client/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from juju.client._definitions import *


from juju.client import _client7, _client1, _client3, _client4, _client2, _client17, _client6, _client11, _client10, _client5, _client9, _client18, _client19
from juju.client import _client7, _client1, _client3, _client4, _client2, _client17, _client6, _client11, _client10, _client5, _client9, _client18, _client19, _client20


CLIENTS = {
Expand All @@ -20,7 +20,8 @@
"5": _client5,
"9": _client9,
"18": _client18,
"19": _client19
"19": _client19,
"20": _client20
}


Expand All @@ -42,6 +43,24 @@ def lookup_facade(name, version):


class TypeFactory:
@classmethod
def from_sync_connection(cls, connection):
facade_name = cls.__name__
if not facade_name.endswith('Facade'):
raise TypeError('Unexpected class name: {}'.format(facade_name))
facade_name = facade_name[:-len('Facade')]
version = connection.facades.get(facade_name)
if version is None:
raise Exception('No facade {} in facades {}'.format(facade_name,
connection.facades))

c = lookup_facade(cls.__name__, version)
c = c()
c.sync_connect(connection)

return c


@classmethod
def from_connection(cls, connection):
"""
Expand Down
Loading
Loading