aboutsummaryrefslogtreecommitdiffstats
path: root/vicn/resource/linux/package_manager.py
blob: 9324150295ed06e7eba26969384ec4fd5e7e7544 (plain)
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 Cisco and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import asyncio
import logging

from netmodel.model.type        import String, Bool
from vicn.core.attribute        import Attribute, Multiplicity
from vicn.core.exception        import ResourceNotFound
from vicn.core.requirement      import Requirement
from vicn.core.resource         import Resource
from vicn.core.task             import BashTask, EmptyTask, async_task
from vicn.core.task             import inline_task, run_task
from vicn.resource.node         import Node

log = logging.getLogger(__name__)

CMD_APT_GET_KILL = 'kill -9 $(pidof apt-get) || true'
CMD_DPKG_CONFIGURE_A = 'dpkg --configure -a'

CMD_APT_GET_UPDATE = '''
# Force IPv4
echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
# Update package repository on node {node}
apt-get update
'''

# We need to double { } we want to preserve
CMD_PKG_TEST='dpkg -s {self.package_name} | grep  "Status:.* install "'

CMD_PKG_INSTALL='''
# Installing package {package_name}
apt-get -y --allow-unauthenticated install {package_name}
'''

CMD_PKG_UNINSTALL='''
# Uninstalling package {self.package_name}
apt-get remove {self.package_name}
'''

CMD_SETUP_REPO = '''
# Initialize package repository {repository.repo_name} on node {self.node.name}
echo "{deb_source}" > {path}
'''

class PackageManager(Resource):
    """
    Resource: PackageManager

    APT package management wrapper.

    Todo:
      - We assume a package manager is always installed on every machine.
      - Currently, we limit ourselves to debian/ubuntu, and voluntarily don't
      subclass this as we have (so far) no code for selecting the right
      subclass, eg choising dynamically between DebRepositoryManager and
      RpmRepositoryManager.
      - We currently don't use package version numbers, which means a package
      can be installed but not be up to date.
    """

    node = Attribute(Node,
            reverse_name = 'package_manager',
            reverse_auto = True,
            mandatory = True,
            key = True,
            multiplicity = Multiplicity.OneToOne)
    trusted = Attribute(Bool,
            description="Force repository trust",
            default=False)

    #--------------------------------------------------------------------------
    # Constructor and Accessors
    #--------------------------------------------------------------------------

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._up_to_date = False
        self.apt_lock = asyncio.Lock()

    #--------------------------------------------------------------------------
    # Resource lifecycle
    #--------------------------------------------------------------------------

    def __after__(self):
        return ('Repository',)

    @inline_task
    def __get__(self):
        raise ResourceNotFound

    #---------------------------------------------------------------------------
    # Methods
    #---------------------------------------------------------------------------

    def __method_setup_repositories__(self):
        repos = EmptyTask()
        for repository in self._state.manager.by_type_str('Repository'):
            deb_source = self._get_deb_source(repository)
            path = self._get_path(repository)
            # XXX There is no need to setup a repo if there is no package to install
            repo = BashTask(self.node, CMD_SETUP_REPO,
                    {'deb_source': deb_source, 'path': path})
            repos = repos | repo

        return repos

    def __method_update__(self):
        kill = BashTask(self.node, CMD_APT_GET_KILL, {'node': self.node.name},
                lock = self.apt_lock)

        # Setup during a reattempt
        if hasattr(self, '_dpkg_configure_a'):
            dpkg_configure_a = BashTask(self.node, CMD_DPKG_CONFIGURE_A,
                    lock = self.apt_lock)
        else:
            dpkg_configure_a = EmptyTask()

        if not self.node.package_manager._up_to_date:
            update = BashTask(self.node, CMD_APT_GET_UPDATE, {'node': self.node.name},
                    lock = self.apt_lock, post = self._mark_updated)
        else:
            update = EmptyTask()

        return (self.__method_setup_repositories__() > (kill > dpkg_configure_a)) > update

    def __method_install__(self, package_name):
        install = BashTask(self.node, CMD_PKG_INSTALL, {'package_name':
                package_name}, lock = self.apt_lock)
        return self.__method_update__() > install

    #---------------------------------------------------------------------------
    # Internal methods
    #---------------------------------------------------------------------------

    def _mark_updated(self):
        self._up_to_date = True

    def _get_path(self, repository):
        return '/etc/apt/sources.list.d/{}.list'.format(repository.repo_name)

    def _get_deb_source(self, repository):
        protocol = 'https' if repository.ssl else 'http'
        path = repository.node.hostname + '/'
        if repository.directory:
            path += repository.directory + '/'
        trusted = '[trusted=yes] ' if self.trusted else ''
        if repository.sections:
            sections = ' {}'.format(' '.join(repository.sections))
        else:
            sections = ''
        if '$DISTRIBUTION' in path:
            path = path.replace('$DISTRIBUTION', self.node.dist)
            return 'deb {}{}://{} ./{}'.format(trusted, protocol, path, sections)
        else:
            return 'deb {}{}://{} {}{}'.format(trusted, protocol, path, self.node.dist, sections)

#------------------------------------------------------------------------------

class Package(Resource):
    """
    Resource: Package

    deb package support
    """

    package_name = Attribute(String, mandatory = True)
    node = Attribute(Node,
            mandatory = True,
            key = True,
            requirements=[
                Requirement('package_manager')
            ])

    #---------------------------------------------------------------------------
    # Resource lifecycle
    #---------------------------------------------------------------------------

    def __get__(self):
        return BashTask(self.node, CMD_PKG_TEST, {'self': self})

    def __create__(self):
        return self.node.package_manager.__method_install__(self.package_name)

    @async_task
    async def __delete__(self):
        with await self.node.package_manager._lock:
            task = BashTask(self.node, CMD_PKG_UNINSTALL, {'self': self})
            ret = await run_task(task, self._state.manager)
            return ret

#------------------------------------------------------------------------------

class Packages(Resource):
    """
    Resource: Packages

    Todo:
     - The number of concurrent subresources is not dynamically linked to the
    nodes. We may need to link subresources to the attribute in general, but
    since package_names are static for a resource, this is not a problem here.
    """
    names = Attribute(String, multiplicity = Multiplicity.OneToMany)
    node = Attribute(Node,
            mandatory = True,
            key = True,
            requirements=[
                Requirement('package_manager')
            ])

    #---------------------------------------------------------------------------
    # Resource lifecycle
    #---------------------------------------------------------------------------

    def __subresources__(self):
        """
        Note: Although packages are (rightfully) specified concurrent, apt tasks
        will be exlusive thanks to the use of a lock in the package manager.
        """
        if self.names:
            packages = [Package(node=self.node, package_name=name, owner=self)
                    for name in self.names]
            return Resource.__concurrent__(*packages)
        else:
            return None