Improve the design spec for the distro hierarchy.
This patch drops the use of separate namespaces for different concerns, such as networking, users and filesystem, instead, it proposes using modules in a distro, as in `distro_name.network`, `distro_name.filesystem` etc. which is better when trying to add new additional distros. Also, it drops the concept of containers-as-creators, instead the objects returned by methods as `routes` or `users` could be implemented in the term of a sequence (for instance a namedtuple). The creation of an object is moved in an object class itself, e.g. Route.create, instead of RouteContainer.create.
This commit is contained in:
parent
027478d8b6
commit
3d0150cbcf
@ -9,9 +9,9 @@ should be established, so that the API could be pythonic, easy to
|
|||||||
comprehend and extend. We have the following examples of how an object
|
comprehend and extend. We have the following examples of how an object
|
||||||
should look, depending on its state and behaviour:
|
should look, depending on its state and behaviour:
|
||||||
|
|
||||||
- Use ``.attribute`` if it the attribute is not changeable
|
- Use ``.attribute`` if the attribute is not changeable
|
||||||
throughout the life of the object.
|
throughout the life of the object.
|
||||||
For instance, the name of a name of a device.
|
For instance, the name of a device.
|
||||||
|
|
||||||
- Use ``.method()`` for obtaining a variant attribute, which can be
|
- Use ``.method()`` for obtaining a variant attribute, which can be
|
||||||
different throughout the execution of the object and not modifiable
|
different throughout the execution of the object and not modifiable
|
||||||
@ -19,13 +19,7 @@ should look, depending on its state and behaviour:
|
|||||||
set a new size and it can vary throughout the life of the device.
|
set a new size and it can vary throughout the life of the device.
|
||||||
|
|
||||||
- For attributes which are modifiable by us and which aren't changing
|
- For attributes which are modifiable by us and which aren't changing
|
||||||
throughout the life of the object, we could use two approaches,
|
throughout the life of the object, we could use a property-based approach.
|
||||||
a property-based approach or a method based approach.
|
|
||||||
|
|
||||||
The first one is more Pythonic, but can
|
|
||||||
hide from the user of that object the fact that the property does
|
|
||||||
more than it says, the second one is more in-your-face,
|
|
||||||
but feels too much like Java's setters and getters and so on:
|
|
||||||
|
|
||||||
>>> device.mtu
|
>>> device.mtu
|
||||||
1500
|
1500
|
||||||
@ -33,16 +27,6 @@ should look, depending on its state and behaviour:
|
|||||||
>>> device.mtu = 1400
|
>>> device.mtu = 1400
|
||||||
1400
|
1400
|
||||||
|
|
||||||
vs
|
|
||||||
|
|
||||||
>>> device.mtu()
|
|
||||||
1500
|
|
||||||
>>> device.set_mtu(1400)
|
|
||||||
>>>
|
|
||||||
|
|
||||||
- similar to the previous point, we could have ``is_something`` or
|
|
||||||
``is_something()``. We must choose one of this variants and use it
|
|
||||||
consistently accross the project.
|
|
||||||
|
|
||||||
|
|
||||||
Proposed distro hierarchy
|
Proposed distro hierarchy
|
||||||
@ -94,29 +78,21 @@ The distros location is proposed, with the following structure and attributes:
|
|||||||
|
|
||||||
- The base class for a distro is found in ``distros.base``.
|
- The base class for a distro is found in ``distros.base``.
|
||||||
|
|
||||||
- There are sub-namespaces for specific interaction with the OS,
|
- There are specific submodules for interaction with the OS,
|
||||||
such as network, users.
|
such as network, users. The submodules are part of distros namespaces,
|
||||||
|
e.g. ``distros.windows`` should contain the modules ``network``,
|
||||||
|
``users`` etc.
|
||||||
|
|
||||||
- More namespaces can be added, if we identify a group of interactions that can
|
- More modules can be added, if we identify a group of interactions that can
|
||||||
be categorized in a namespace.
|
be categorized in one.
|
||||||
|
|
||||||
- There is a ``general`` namespace, which contains general utilities that can't be moved
|
- There should be ``general`` module, which contains general utilities that can't be moved
|
||||||
in another namespace.
|
in another module.
|
||||||
|
|
||||||
- Each subnamespace has its own abstract base class, which must be implemented
|
- Each submodule has its own abstract base class, which must be implemented
|
||||||
by different distros. Code reuse between distros is recommended.
|
by each distro. Code reuse between distros is recommended.
|
||||||
|
|
||||||
- Each subnamespace exposes a way to obtain an instance of that namespace for
|
- Each submodule can expose additional behaviour that might not exist in
|
||||||
the underlying distro. This can be exposed in namespace.factory.
|
|
||||||
Thus, the following are equivalent:
|
|
||||||
|
|
||||||
>>> from cloudinit import distro
|
|
||||||
>>> network = distro.get_distro().network
|
|
||||||
|
|
||||||
>>> from cloudinit.distro.network import factory
|
|
||||||
>>> network = factory.get_network()
|
|
||||||
|
|
||||||
- Each subnamespace can expose additional behaviour that might not exist in
|
|
||||||
the base class, if that behaviour does not make sense or if there is no
|
the base class, if that behaviour does not make sense or if there is no
|
||||||
equivalent on other platforms. But don't expose leaky abstraction, this
|
equivalent on other platforms. But don't expose leaky abstraction, this
|
||||||
specific code must be written in an abstract way, so that possible alternatives
|
specific code must be written in an abstract way, so that possible alternatives
|
||||||
@ -127,55 +103,27 @@ The distros location is proposed, with the following structure and attributes:
|
|||||||
|
|
||||||
cloudinit/distros/__init__.py
|
cloudinit/distros/__init__.py
|
||||||
base.py
|
base.py
|
||||||
factory.py
|
|
||||||
|
|
||||||
network/
|
freebsd/
|
||||||
factory.py
|
__init__.py
|
||||||
base.py
|
network.py
|
||||||
windows.py
|
users.py
|
||||||
freebsd.py
|
general.py
|
||||||
debian.py
|
filesystem.py
|
||||||
|
windows/
|
||||||
|
__init__.py
|
||||||
|
network.py
|
||||||
|
users.py
|
||||||
|
general.py
|
||||||
|
ubuntu/
|
||||||
|
__init__.py
|
||||||
|
network.py
|
||||||
....
|
....
|
||||||
__init__.py
|
|
||||||
|
|
||||||
users
|
|
||||||
factory.py
|
|
||||||
base.py
|
|
||||||
freebsd.py
|
|
||||||
debian.py
|
|
||||||
....
|
|
||||||
__init__.py
|
|
||||||
|
|
||||||
filesystem
|
|
||||||
factory.py
|
|
||||||
base.py
|
|
||||||
freebsd.py
|
|
||||||
...
|
|
||||||
__init__.py
|
|
||||||
|
|
||||||
packaging
|
|
||||||
factory.py
|
|
||||||
base.py
|
|
||||||
freebsd.py
|
|
||||||
debian.py
|
|
||||||
....
|
|
||||||
__init__.py
|
|
||||||
|
|
||||||
general
|
|
||||||
base.py
|
|
||||||
windows.py
|
|
||||||
debian.py
|
|
||||||
....
|
|
||||||
__init__.py
|
|
||||||
|
|
||||||
|
|
||||||
The base class for the Distro specific implementation must provide
|
>>> from cloudinit.distros.base import get_distro
|
||||||
an accessor member for each namespace, so that it will be sufficient
|
>>> distro = get_distro()
|
||||||
to obtain the distro in order to have each namespace.
|
>>> distro.network # the actual object, not the submodule
|
||||||
|
|
||||||
>>> from cloudinit.distros import factory
|
|
||||||
>>> distro = factory.get_distro()
|
|
||||||
>>> distro.network # the actual object, not the subpackage
|
|
||||||
<WindowsNetwork:/distro/network/windows>
|
<WindowsNetwork:/distro/network/windows>
|
||||||
>>> distro.users
|
>>> distro.users
|
||||||
<WindowsUsers:/distro/users/windows>
|
<WindowsUsers:/distro/users/windows>
|
||||||
@ -190,8 +138,8 @@ distro can use a combination of `platform.system`_ and `platform.linux_distribut
|
|||||||
In the following, I'll try to emphasize some possible APIs for each namespace.
|
In the following, I'll try to emphasize some possible APIs for each namespace.
|
||||||
|
|
||||||
|
|
||||||
Network subnamespace
|
Network module
|
||||||
----------------------
|
--------------
|
||||||
|
|
||||||
The abstract class can look like this:
|
The abstract class can look like this:
|
||||||
|
|
||||||
@ -203,13 +151,35 @@ Network subnamespace
|
|||||||
Each route should be an object encapsulating the inner workings
|
Each route should be an object encapsulating the inner workings
|
||||||
of each variant.
|
of each variant.
|
||||||
|
|
||||||
So :meth:`routes` returns ``RouteContainer([Route(...), Route(...), Route(...))``
|
:meth:`routes` returns an object with behaviour similar to that
|
||||||
See the description for :class:`RouteContainer` for more details,
|
of a sequence (it could be implemented using collections.Sequence
|
||||||
as well as the description of :class:`Route` for the API of the route object.
|
or something similar, as long as it guarantees an interface).
|
||||||
|
See the description of :class:`Route` for the API of the route object.
|
||||||
|
|
||||||
Using ``route in network.routes()`` and ``network.routes().add(route)
|
The following behaviour should be supported by the object returned by
|
||||||
removes the need for ``cloudbaseinit.osutils.check_static_route_exists``
|
:meth:`routes`.
|
||||||
and ``cloudbaseinit.osutils.add_static_route``.
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Support iteration."""
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
"""Support containment."""
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
"""Support element access"""
|
||||||
|
|
||||||
|
Some API usages:
|
||||||
|
|
||||||
|
>>> routes = network.routes()
|
||||||
|
>>> route_object in routes
|
||||||
|
True
|
||||||
|
>>> '192.168.70.14' in routes
|
||||||
|
False
|
||||||
|
>>> route = Route.from_route_entry(
|
||||||
|
"0.0.0.0 192.168.60.2 "
|
||||||
|
"0.0.0.0 UG 0 0 "
|
||||||
|
"0 eth0")
|
||||||
|
>>> route.delete()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def default_gateway(self):
|
def default_gateway(self):
|
||||||
@ -222,7 +192,7 @@ Network subnamespace
|
|||||||
"""Get the network interfaces
|
"""Get the network interfaces
|
||||||
|
|
||||||
This can be implemented in the same vein as :meth:`routes`, e.g.
|
This can be implemented in the same vein as :meth:`routes`, e.g.
|
||||||
``InterfaceContainer([Interface(...), Interface(...), Interface(...)])``
|
``sequence(Interface(...), Interface(...), ...)``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def firewall_rules(self):
|
def firewall_rules(self):
|
||||||
@ -233,12 +203,12 @@ Network subnamespace
|
|||||||
The same behaviour as for :meth:`routes` can be used, that is:
|
The same behaviour as for :meth:`routes` can be used, that is:
|
||||||
|
|
||||||
>>> rules = distro.network.firewall_rules()
|
>>> rules = distro.network.firewall_rules()
|
||||||
|
# Creating a new rule.
|
||||||
>>> rule = distro.network.FirewallRule(name=..., port=..., protocol=...)
|
>>> rule = distro.network.FirewallRule(name=..., port=..., protocol=...)
|
||||||
>>> rules.add(rule)
|
# Deleting a rule
|
||||||
>>> rules.delete(rule)
|
>>> rule.delete()
|
||||||
>>> rule in rules
|
>>> rule in rules
|
||||||
>>> for rule in rules: print(rules)
|
>>> for rule in rules: print(rules)
|
||||||
>>> del rules[i]
|
|
||||||
>>> rule = rules[0]
|
>>> rule = rules[0]
|
||||||
>>> rule.name, rule.port, rule.protocol, rule.allow
|
>>> rule.name, rule.port, rule.protocol, rule.allow
|
||||||
|
|
||||||
@ -264,8 +234,11 @@ Network subnamespace
|
|||||||
|
|
||||||
|
|
||||||
>>> hosts = distro.network.hosts()
|
>>> hosts = distro.network.hosts()
|
||||||
>>> list(hosts) # support iteration and index access
|
# Add a new entry in the hosts file, as well
|
||||||
|
# in the object container itself
|
||||||
>>> hosts.add(ipaddress, hostname, alias)
|
>>> hosts.add(ipaddress, hostname, alias)
|
||||||
|
# Delete an entry from the hosts file and from
|
||||||
|
# the object container itself
|
||||||
>>> hosts.delete(ipaddress, hostname, alias)
|
>>> hosts.delete(ipaddress, hostname, alias)
|
||||||
|
|
||||||
This gets rid of ``cloudinit.distros.Distro.update_etc_hosts``
|
This gets rid of ``cloudinit.distros.Distro.update_etc_hosts``
|
||||||
@ -286,6 +259,10 @@ Network subnamespace
|
|||||||
route.expire
|
route.expire
|
||||||
route.static -> 'S' in self.flags
|
route.static -> 'S' in self.flags
|
||||||
route.usable -> 'U' in self.flag
|
route.usable -> 'U' in self.flag
|
||||||
|
|
||||||
|
This can use a namedtuple as a base, but this should
|
||||||
|
be considered an implementation detail by the users
|
||||||
|
of this class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -296,57 +273,6 @@ Network subnamespace
|
|||||||
from `GetIpForwardTable`.
|
from `GetIpForwardTable`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class RouteContainer(object):
|
|
||||||
"""A wrapper over the result from :meth:`NetworkBase.routes()`,
|
|
||||||
which provides some OO interaction with the underlying routes.
|
|
||||||
|
|
||||||
|
|
||||||
>>> routes = network.routes() # a RouteContainer
|
|
||||||
>>> route_object in routes
|
|
||||||
True
|
|
||||||
>>> '192.168.70.14' in routes
|
|
||||||
False
|
|
||||||
>>> route = Route.from_route_entry(
|
|
||||||
"0.0.0.0 192.168.60.2 "
|
|
||||||
"0.0.0.0 UG 0 0 "
|
|
||||||
"0 eth0")
|
|
||||||
>>> routes.add(route)
|
|
||||||
>>> routes.delete(route)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Support iteration."""
|
|
||||||
|
|
||||||
def __contains__(self, item):
|
|
||||||
"""Support containment."""
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
"""Support element access"""
|
|
||||||
|
|
||||||
def __delitem__(self, item):
|
|
||||||
"""Delete a route, equivalent to :meth:`delete_route`."""
|
|
||||||
|
|
||||||
def __iadd__(self, item):
|
|
||||||
"""Add route, equivalent to :meth:``add_route``."""
|
|
||||||
|
|
||||||
def add(self, route):
|
|
||||||
"""Add a new route."""
|
|
||||||
|
|
||||||
def delete(self, destination, mask, metric, ...):
|
|
||||||
"""Delete a route."""
|
|
||||||
|
|
||||||
class InterfaceContainer(object):
|
|
||||||
"""Container for interfaces, with similar API as for RouteContainer."""
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Support iteration."""
|
|
||||||
|
|
||||||
def __contains__(self, item):
|
|
||||||
"""Support containment."""
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
"""Support element access"""
|
|
||||||
|
|
||||||
class Interface(object):
|
class Interface(object):
|
||||||
"""Encapsulation for the state and behaviour of an interface.
|
"""Encapsulation for the state and behaviour of an interface.
|
||||||
|
|
||||||
@ -369,10 +295,10 @@ Network subnamespace
|
|||||||
obtain a :class:`Interface` instance from it.
|
obtain a :class:`Interface` instance from it.
|
||||||
|
|
||||||
>>> interface = distro.network.Interface.from_name('eth0')
|
>>> interface = distro.network.Interface.from_name('eth0')
|
||||||
>>> nterface = distro.network.Interface.from_mac( u'00:50:56:C0:00:01')
|
>>> interface = distro.network.Interface.from_mac( u'00:50:56:C0:00:01')
|
||||||
|
|
||||||
Each Distro specific implementation of :class:`Interface` should
|
Each Distro specific implementation of :class:`Interface` should
|
||||||
be exported in the `network` namespace as the `Interface` attribute,
|
be exported in the `network` module as the `Interface` attribute,
|
||||||
so that the underlying OS is completely hidden from an API point-of-view.
|
so that the underlying OS is completely hidden from an API point-of-view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -415,8 +341,8 @@ Network subnamespace
|
|||||||
TODO: finish this section with APis for set_hostname, _read_hostname, update_hostname
|
TODO: finish this section with APis for set_hostname, _read_hostname, update_hostname
|
||||||
|
|
||||||
|
|
||||||
Users subnamespace
|
Users module
|
||||||
------------------
|
------------
|
||||||
|
|
||||||
The base class for this namespace can look like this
|
The base class for this namespace can look like this
|
||||||
|
|
||||||
@ -429,20 +355,18 @@ The base class for this namespace can look like this
|
|||||||
Similar with network.routes() et al, that is
|
Similar with network.routes() et al, that is
|
||||||
|
|
||||||
>>> groups = distro.users.groups()
|
>>> groups = distro.users.groups()
|
||||||
GroupContainer(Group(...), Group(....), ...)
|
sequence(Group(...), Group(....), ...)
|
||||||
# create a new group
|
# create a new group
|
||||||
>>> group = distro.users.Group.create(name)
|
>>> group = distro.users.Group.create(name)
|
||||||
# Add new members to a group
|
# Add new members to a group
|
||||||
>>> group.add(member)
|
>>> group.add(member)
|
||||||
# Add a new group
|
|
||||||
>>> groups.add(group)
|
|
||||||
# Remove a group
|
# Remove a group
|
||||||
>>> groups.delete(group)
|
>>> group.delete()
|
||||||
# Iterate groups
|
# Iterate groups
|
||||||
>>> list(groups)
|
>>> list(groups)
|
||||||
|
|
||||||
This gets rid of ``cloudinit.distros.Distro.create_group``,
|
This gets rid of ``cloudinit.distros.Distro.create_group``,
|
||||||
which creates a group and adds member to it as well and it get rids of
|
which creates a group and adds members to it as well and it get rids of
|
||||||
``cloudbaseinit.osutils.add_user_to_local``.
|
``cloudbaseinit.osutils.add_user_to_local``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -456,13 +380,13 @@ The base class for this namespace can look like this
|
|||||||
>>> user in users
|
>>> user in users
|
||||||
# Iteration
|
# Iteration
|
||||||
>>> for i in user: print(user)
|
>>> for i in user: print(user)
|
||||||
# Add a new user
|
|
||||||
>>> user = users.create(username, password, password_expires=False)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class User:
|
class User:
|
||||||
""" Abstracts away user interaction.
|
""" Abstracts away user interaction.
|
||||||
|
|
||||||
|
# Creating a new user.
|
||||||
|
>>> User.create(username=..., password=..., ...)
|
||||||
# get the home dir of an user
|
# get the home dir of an user
|
||||||
>>> user.home()
|
>>> user.home()
|
||||||
# Get the password (?)
|
# Get the password (?)
|
||||||
@ -484,8 +408,8 @@ The base class for this namespace can look like this
|
|||||||
|
|
||||||
TODO: what is cloudinit.distros.get_default_user?
|
TODO: what is cloudinit.distros.get_default_user?
|
||||||
|
|
||||||
Packaging namespace
|
Packaging module
|
||||||
-------------------
|
----------------
|
||||||
|
|
||||||
This object is a thin layer over Distro specific packaging utilities,
|
This object is a thin layer over Distro specific packaging utilities,
|
||||||
used in cloudinit through ``distro.Distro.package_command``.
|
used in cloudinit through ``distro.Distro.package_command``.
|
||||||
@ -504,8 +428,8 @@ we could have a more OO approach:
|
|||||||
On Windows side, this can be implemented with OneGet.
|
On Windows side, this can be implemented with OneGet.
|
||||||
|
|
||||||
|
|
||||||
Filesystem namespace
|
Filesystem module
|
||||||
--------------------
|
-----------------
|
||||||
|
|
||||||
Layer over filesystem interaction specific for each OS.
|
Layer over filesystem interaction specific for each OS.
|
||||||
Most of the uses encountered are related to the concept of devices and partitions.
|
Most of the uses encountered are related to the concept of devices and partitions.
|
||||||
@ -552,8 +476,8 @@ class FilesystemBase(ABC):
|
|||||||
>>> partition = DevicePartition.from_name('sda1')
|
>>> partition = DevicePartition.from_name('sda1')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
General namespace
|
General module
|
||||||
-----------------
|
--------------
|
||||||
|
|
||||||
Here we could have other general OS utilities: terminate, apply_locale,
|
Here we could have other general OS utilities: terminate, apply_locale,
|
||||||
set_timezone, execute_process etc. If some utilities can be grouped
|
set_timezone, execute_process etc. If some utilities can be grouped
|
||||||
|
Loading…
Reference in New Issue
Block a user