0002-environment-add-migrate_service-command-58908.patch
hobo/environment/management/commands/cook.py | ||
---|---|---|
20 | 20 |
import os |
21 | 21 |
import string |
22 | 22 |
import subprocess |
23 |
import sys |
|
24 |
import time |
|
25 | 23 | |
26 | 24 |
from django.contrib.auth.models import User |
27 | 25 |
from django.contrib.contenttypes.models import ContentType |
... | ... | |
50 | 48 |
Wcs, |
51 | 49 |
Welco, |
52 | 50 |
) |
51 |
from hobo.environment.utils import wait_operationals |
|
53 | 52 |
from hobo.environment.validators import validate_service_url |
54 | 53 |
from hobo.multitenant.middleware import TenantMiddleware |
55 | 54 |
from hobo.profile.models import AttributeDefinition |
... | ... | |
79 | 78 |
self.verbosity = kwargs.get('verbosity') |
80 | 79 |
self.timeout = kwargs.get('timeout') |
81 | 80 |
self.permissive = kwargs.get('permissive') |
81 |
self.terminal_width = 0 |
|
82 | 82 |
if self.verbosity > 1: |
83 | 83 |
try: |
84 | 84 |
self.terminal_width = int(subprocess.check_output(['tput', 'cols']).strip()) |
... | ... | |
119 | 119 |
services = [] |
120 | 120 |
for service_class in AVAILABLE_SERVICES: |
121 | 121 |
services.extend(service_class.objects.all()) |
122 | ||
123 |
t0 = time.time() |
|
124 |
i = 0 |
|
125 |
last_service_url = None |
|
126 |
last_notification = t0 |
|
127 |
while len(services) > 0: |
|
128 |
if time.time() - last_notification > 15: |
|
129 |
last_notification = time.time() |
|
130 |
notify_agents(None) |
|
131 |
for service in services[:]: |
|
132 |
if service.last_operational_success_timestamp: |
|
133 |
services.remove(service) |
|
134 |
continue |
|
135 |
service.check_operational() |
|
136 |
if len(services) == 0: |
|
137 |
break |
|
138 |
if self.verbosity == 1: |
|
139 |
sys.stderr.write('.') |
|
140 |
elif self.verbosity > 1: |
|
141 |
if last_service_url != services[0].base_url: |
|
142 |
last_service_url = services[0].base_url |
|
143 |
i = 0 |
|
144 |
elif i == (self.terminal_width - len(services[0].base_url) - 25): |
|
145 |
i = 0 |
|
146 |
i += 1 |
|
147 |
sys.stderr.write('[2K\rWaiting for %s ' % services[0].base_url) |
|
148 |
sys.stderr.write('%5ds ' % (timeout - (time.time() - t0))) |
|
149 |
sys.stderr.write('.' * i) |
|
150 |
sys.stderr.flush() |
|
151 |
time.sleep(0.5) |
|
152 |
if time.time() - t0 > timeout: |
|
153 |
if self.verbosity: |
|
154 |
sys.stderr.write('\n') |
|
155 |
raise CommandError('timeout waiting for %s' % ', '.join([x.base_url for x in services])) |
|
122 |
wait_operationals(services, timeout, self.verbosity, self.terminal_width, notify_agents) |
|
156 | 123 | |
157 | 124 |
def create_hobo(self, url, primary=False, title=None, slug=None, **kwargs): |
158 | 125 |
if connection.get_tenant().schema_name == 'public': |
hobo/environment/management/commands/migrate_service.py | ||
---|---|---|
1 |
# hobo - portal to configure and deploy applications |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import subprocess |
|
18 | ||
19 |
from django.core.management.base import BaseCommand, CommandError |
|
20 | ||
21 |
from hobo.deploy.signals import notify_agents |
|
22 |
from hobo.environment.utils import get_installed_services, wait_operationals |
|
23 | ||
24 | ||
25 |
def normalize_url(url): |
|
26 |
if not url.endswith('/'): |
|
27 |
url += '/' |
|
28 |
return url |
|
29 | ||
30 | ||
31 |
class Command(BaseCommand): |
|
32 |
verbosity = 1 |
|
33 | ||
34 |
def add_arguments(self, parser): |
|
35 |
parser.add_argument('src_url', type=str) |
|
36 |
parser.add_argument('target_url', type=str) |
|
37 |
parser.add_argument( |
|
38 |
'--timeout', |
|
39 |
type=int, |
|
40 |
action='store', |
|
41 |
default=120, |
|
42 |
help='set the timeout for the wait_operationals method', |
|
43 |
) |
|
44 | ||
45 |
def handle(self, src_url, target_url, *args, **kwargs): |
|
46 |
timeout = kwargs.get('timeout') |
|
47 |
verbosity = kwargs.get('verbosity') |
|
48 |
terminal_width = 0 |
|
49 |
if self.verbosity > 1: |
|
50 |
try: |
|
51 |
terminal_width = int(subprocess.check_output(['tput', 'cols']).strip()) |
|
52 |
except OSError: |
|
53 |
terminal_width = 80 |
|
54 | ||
55 |
src_url, target_url = normalize_url(src_url), normalize_url(target_url) |
|
56 |
target_service = None |
|
57 | ||
58 |
for service in get_installed_services(): |
|
59 |
if service.get_base_url_path() == src_url: |
|
60 |
target_service = service |
|
61 |
break |
|
62 |
if target_service is None: |
|
63 |
raise CommandError('No service matches %s' % src_url) |
|
64 |
if target_service.Extra.service_id in ('authentic', 'hobo', 'wcs'): |
|
65 |
raise CommandError("%s service type is not supported" % target_service.Extra.service_id) |
|
66 | ||
67 |
target_service.change_base_url(target_url) |
|
68 |
target_service.last_operational_check_timestamp = None |
|
69 |
target_service.last_operational_success_timestamp = None |
|
70 |
target_service.save() |
|
71 |
notify_agents(None) |
|
72 |
wait_operationals([target_service], timeout, verbosity, terminal_width, notify_agents) |
|
73 |
if self.verbosity: |
|
74 |
print('Service migrated successfully, check it out : %s' % target_service.get_base_url_path()) |
hobo/environment/utils.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
import hashlib |
18 |
import sys |
|
19 |
import time |
|
18 | 20 | |
19 | 21 |
from django.conf import settings |
22 |
from django.core.management.base import CommandError |
|
20 | 23 |
from django.db import connection, transaction |
21 | 24 |
from django.urls import reverse |
22 | 25 |
from django.utils.encoding import force_text |
... | ... | |
185 | 188 |
for key, value in fields.items(): |
186 | 189 |
setattr(obj, key, value) |
187 | 190 |
obj.save() |
191 | ||
192 | ||
193 |
def wait_operationals(services, timeout, verbosity, terminal_width, notify_agents_func): |
|
194 |
t0 = time.time() |
|
195 |
i = 0 |
|
196 |
last_service_url = None |
|
197 |
last_notification = t0 |
|
198 |
while len(services) > 0: |
|
199 |
if time.time() - last_notification > 15: |
|
200 |
last_notification = time.time() |
|
201 |
notify_agents_func(None) |
|
202 |
for service in services[:]: |
|
203 |
if service.last_operational_success_timestamp: |
|
204 |
services.remove(service) |
|
205 |
continue |
|
206 |
service.check_operational() |
|
207 |
if len(services) == 0: |
|
208 |
break |
|
209 |
if verbosity == 1: |
|
210 |
sys.stderr.write('.') |
|
211 |
elif verbosity > 1: |
|
212 |
if last_service_url != services[0].base_url: |
|
213 |
last_service_url = services[0].base_url |
|
214 |
i = 0 |
|
215 |
elif i == (terminal_width - len(services[0].base_url) - 25): |
|
216 |
i = 0 |
|
217 |
i += 1 |
|
218 |
sys.stderr.write('[2K\rWaiting for %s ' % services[0].base_url) |
|
219 |
sys.stderr.write('%5ds ' % (timeout - (time.time() - t0))) |
|
220 |
sys.stderr.write('.' * i) |
|
221 |
sys.stderr.flush() |
|
222 |
time.sleep(0.5) |
|
223 |
if time.time() - t0 > timeout: |
|
224 |
if verbosity: |
|
225 |
sys.stderr.write('\n') |
|
226 |
raise CommandError('timeout waiting for %s' % ', '.join([x.base_url for x in services])) |
tests/test_cook.py | ||
---|---|---|
179 | 179 |
# already operational |
180 | 180 |
obj1.last_operational_success_timestamp = 'some date' |
181 | 181 |
obj2.last_operational_success_timestamp = 'some date' |
182 |
command.terminal_width = 80 |
|
182 | 183 |
command.wait_operationals(2) |
183 | 184 |
assert True |
184 | 185 |
tests_schemas/conftest.py | ||
---|---|---|
8 | 8 |
from tenant_schemas.utils import tenant_context |
9 | 9 | |
10 | 10 |
from hobo.environment.management.commands.cook import Command |
11 |
from hobo.environment.management.commands.migrate_service import Command as MigrateServiceCommand |
|
11 | 12 |
from hobo.multitenant.middleware import TenantMiddleware |
12 | 13 | |
13 | 14 |
tests_schemas/test_hobo_deploy.py | ||
---|---|---|
54 | 54 |
assert_deployed(domain) |
55 | 55 | |
56 | 56 | |
57 |
def test_deploy_specifics_on_hobo_agent(db): |
|
57 |
def test_deploy_specifics_on_hobo_agent(transactional_db):
|
|
58 | 58 |
"""overloaded case for hobo: |
59 | 59 |
$ hobo-manage hobo-deploy env.json # simulate this bash query |
60 | 60 |
""" |
tests_schemas/test_migrate_service.py | ||
---|---|---|
1 |
import os |
|
2 | ||
3 |
import pytest |
|
4 |
from django.core.management import call_command, load_command_class |
|
5 |
from django.core.management.base import CommandError |
|
6 |
from mock import Mock |
|
7 |
from tenant_schemas.utils import tenant_context |
|
8 | ||
9 |
from hobo.environment.models import Chrono |
|
10 |
from hobo.environment.utils import get_installed_services |
|
11 |
from hobo.multitenant.middleware import TenantMiddleware |
|
12 | ||
13 | ||
14 |
def assert_deployed(domain): |
|
15 |
tenant = TenantMiddleware.get_tenant_by_hostname(domain) |
|
16 |
tenant_hobo_json = os.path.join(tenant.get_directory(), 'hobo.json') |
|
17 |
assert os.path.exists(tenant_hobo_json) |
|
18 | ||
19 | ||
20 |
@pytest.fixture() |
|
21 |
def hobo_tenant(db): |
|
22 |
name = 'hobo.dev.publik.love' |
|
23 |
yield call_command('create_hobo_tenant', name) |
|
24 |
call_command('delete_tenant', name) |
|
25 | ||
26 | ||
27 |
def test_unknown_service(hobo_tenant): |
|
28 |
command = load_command_class('hobo.agent.hobo', 'hobo_deploy') |
|
29 |
domain = 'hobo.dev.publik.love' |
|
30 | ||
31 |
command.handle('https://%s/' % domain, 'tests_schemas/env.json') |
|
32 |
assert_deployed(domain) |
|
33 | ||
34 |
tenant = TenantMiddleware.get_tenant_by_hostname('hobo.dev.publik.love') |
|
35 |
with tenant_context(tenant): |
|
36 |
assert get_installed_services() |
|
37 |
with pytest.raises(CommandError) as e_info: |
|
38 |
call_command( |
|
39 |
'migrate_service', 'https://unkown.dev.publik.love/', 'https://new-chrono.dev.publik.love/' |
|
40 |
) |
|
41 |
assert 'No service matches https://unkown.dev.publik.love/' in str(e_info.value) |
|
42 | ||
43 | ||
44 |
def test_unsupported_service(hobo_tenant): |
|
45 |
command = load_command_class('hobo.agent.hobo', 'hobo_deploy') |
|
46 |
domain = 'hobo.dev.publik.love' |
|
47 |
command.handle('https://%s/' % domain, 'tests_schemas/env.json') |
|
48 |
assert_deployed(domain) |
|
49 | ||
50 |
tenant = TenantMiddleware.get_tenant_by_hostname('hobo.dev.publik.love') |
|
51 |
with tenant_context(tenant): |
|
52 |
assert get_installed_services() |
|
53 |
with pytest.raises(CommandError) as e_info: |
|
54 |
call_command( |
|
55 |
'migrate_service', 'https://wcs.dev.publik.love/', 'https://new-wcs.dev.publik.love/' |
|
56 |
) |
|
57 |
assert 'wcs service type is not supported' in str(e_info.value) |
|
58 | ||
59 | ||
60 |
def test_change_base_url(hobo_tenant, monkeypatch): |
|
61 |
deploy_command = load_command_class('hobo.agent.hobo', 'hobo_deploy') |
|
62 |
domain = 'hobo.dev.publik.love' |
|
63 |
deploy_command.handle('https://%s/' % domain, 'tests_schemas/env.json') |
|
64 |
assert_deployed(domain) |
|
65 | ||
66 |
tenant = TenantMiddleware.get_tenant_by_hostname(domain) |
|
67 |
with tenant_context(tenant): |
|
68 |
assert Chrono.objects.count() == 1 |
|
69 |
chrono_service = Chrono.objects.first() |
|
70 |
assert chrono_service.get_base_url_path() == 'https://chrono.dev.publik.love/' |
|
71 |
assert get_installed_services() |
|
72 |
monkeypatch.setattr('hobo.environment.management.commands.migrate_service.notify_agents', Mock()) |
|
73 |
monkeypatch.setattr('hobo.environment.management.commands.migrate_service.wait_operationals', Mock()) |
|
74 |
call_command( |
|
75 |
'migrate_service', 'https://chrono.dev.publik.love/', 'https://new-chrono.dev.publik.love/' |
|
76 |
) |
|
77 |
assert Chrono.objects.count() == 1 |
|
78 |
chrono_service = Chrono.objects.first() |
|
79 |
assert chrono_service.get_base_url_path() == 'https://new-chrono.dev.publik.love/' |
|
80 |
assert len(chrono_service.legacy_urls) == 1 |
|
81 |
assert chrono_service.legacy_urls[0]['base_url'] == 'https://chrono.dev.publik.love/' |
|
0 |
- |