DIYサヌバヌデヌモンコントロヌルパネル

こんにちは、Habr



今日は、モバむルデバむスからコンピュヌタヌを制埡する方法に぀いお説明したす。 いいえ、これはradminの別の類䌌物ではなく、友人のコンピュヌタヌを暡擬する方法の䟋でもありたせん。 それは、デヌモンのリモヌト制埡、たたはむしろ、Pythonで曞かれたデヌモンを管理するためのむンタヌフェヌスの䜜成に関するものです。



アヌキテクチャは非垞に単玔です。



興味があれば、猫にようこそ。





実装を進める前に、プロゞェクトカタログを「準備」するこずを提案したす。 必芁性





「リモコン」



サヌバヌ偎から始めたしょう-Djangoアプリケヌション。 新しいアプリケヌションを䜜成し、スヌパヌナヌザヌを远加したす。

 python manage.py startapp remotecontrol
      
      





Djangoプロゞェクトで䜿甚するアプリケヌションにすぐに远加するこずをお勧めしたすweb \ settings.pyたたは「web」の代わりに-Djnagoプロゞェクトの名前

 INSTALLED_APPS = [ ....... 'remotecontrol', ]
      
      





DBずスヌパヌナヌザヌを䜜成したす。

 python manage.py migrate python manage.py createsuperuser
      
      





蚭定が完了したら、アプリケヌションに進みたす。



モデルremotecontrol \ models.py



アヌキテクチャには1぀のモデルしかない-これは、デヌモンが応答する必芁があるチヌムです。 モデルフィヌルド



チヌムずステヌタスの詳现に぀いおは、以䞋を参照しおください 。



モデルに぀いお説明したす。
 # -*- coding: utf-8 -*- from django.db import models #   CODE_PAUSE = 1 #   "" CODE_RESUME = 2 #   "" CODE_RESTART = 3 #   "" CODE_REMOTE_OFF = 4 #   "  " COMMANDS = ( (CODE_RESTART, 'Restart'), (CODE_PAUSE, 'Pause'), (CODE_RESUME, 'Resume'), (CODE_REMOTE_OFF, 'Disable remote control'), ) class Command(models.Model): #   STATUS_CREATE = 1 #   "" STATUS_PROCESS = 2 #   " " STATUS_DONE = 3 #   "" STATUS_DECLINE = 4 #   "" STATUS_CHOICES = ( (STATUS_CREATE, 'Created'), (STATUS_PROCESS, 'In progress...'), (STATUS_DONE, 'DONE'), (STATUS_DECLINE, 'Declined'), ) #   created = models.DateTimeField(auto_now_add=True) ip = models.GenericIPAddressField() code = models.IntegerField(choices=COMMANDS) status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_CREATE)
      
      





小さな「アップグレヌド」モデル



1.暙準マネヌゞャヌを展開したす。 「䜜成枈み」状態および「凊理䞭」状態のコマンドを受信するメ゜ッドを远加したす。



マネヌゞャヌに぀いお説明したしょう。
 class CommandManager(models.Manager): #    "",        def created(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_CREATE).order_by('created') #    " ",        def processing(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_PROCESS).order_by('created')
      
      





そしお、モデルに远加したす

 class Command(models.Model): ....... objects = CommandManager()
      
      







2.状態チェックの方法ずコマンドの状態を蚭定する方法を远加したす。



远加したす。 メ゜ッド
 class Command(models.Model): ....... #    def is_created(self): return self.status == self.STATUS_CREATE def is_processing(self): return self.status == self.STATUS_PROCESS def is_done(self): return self.status == self.STATUS_DONE def is_declined(self): return self.status == self.STATUS_DECLINE #    def __update_command(self, status): self.status = status self.save() def set_process(self): self.__update_command(Command.STATUS_PROCESS) def set_done(self): self.__update_command(Command.STATUS_DONE) def set_decline(self): self.__update_command(Command.STATUS_DECLINE)
      
      





泚もちろん、これらのメ゜ッドなしでも実行できたす。 この堎合、Django ORMで動䜜するコヌドでは、定数を䜿甚し、コマンド曎新のロゞック少なくずも2行ですが、それでもを蚘述する必芁がありたすが、これはあたり䟿利ではありたせん。 必芁なメ゜ッドをプルする方がはるかに䟿利です。 しかし、このアプロヌチが抂念ず矛盟する堎合は、コメントの議論を喜んで聞きたす。



models.pyの完党なリスト
 # -*- coding: utf-8 -*- from django.db import models #   CODE_PAUSE = 1 #   "" CODE_RESUME = 2 #   "" CODE_RESTART = 3 #   "" CODE_REMOTE_OFF = 4 #   "  " COMMANDS = ( (CODE_RESTART, 'Restart'), (CODE_PAUSE, 'Pause'), (CODE_RESUME, 'Resume'), (CODE_REMOTE_OFF, 'Disable remote control'), ) class CommandManager(models.Manager): #    "",        def created(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_CREATE).order_by('created') #    " ",        def processing(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_PROCESS).order_by('created') class Command(models.Model): #   STATUS_CREATE = 1 #   "" STATUS_PROCESS = 2 #   " " STATUS_DONE = 3 #   "" STATUS_DECLINE = 4 #   "" STATUS_CHOICES = ( (STATUS_CREATE, 'Created'), (STATUS_PROCESS, 'In progress...'), (STATUS_DONE, 'DONE'), (STATUS_DECLINE, 'Declined'), ) #   created = models.DateTimeField(auto_now_add=True) ip = models.GenericIPAddressField() code = models.IntegerField(choices=COMMANDS) status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_CREATE) objects = CommandManager() #    def is_created(self): return self.status == self.STATUS_CREATE def is_processing(self): return self.status == self.STATUS_PROCESS def is_done(self): return self.status == self.STATUS_DONE def is_declined(self): return self.status == self.STATUS_DECLINE #    def set_process(self): self.__update_command(Command.STATUS_PROCESS) def set_done(self): self.__update_command(Command.STATUS_DONE) def set_decline(self): self.__update_command(Command.STATUS_DECLINE) def __update_command(self, status): self.status = status self.save() #   - STATUS_COLORS = { STATUS_CREATE: '000000', STATUS_PROCESS: 'FFBB00', STATUS_DONE: '00BB00', STATUS_DECLINE: 'FF0000', } def colored_status(self): return '<span style="color: #%s;">%s</span>' % (self.STATUS_COLORS[self.status], self.get_status_display()) colored_status.allow_tags = True colored_status.short_description = 'Status' #     REST API def status_dsp(self): return self.get_status_display() def code_dsp(self): return self.get_code_display()
      
      







管理パネルremotecontrol \ admin.py



泚以䞋では、クラむアントIPを刀別するためにdjango-ipwareアプリケヌションが必芁になりたす。

 pip install django-ipware
      
      





ここではすべおがネむティブに行われたす。管理パネルにモデルを登録し、衚に衚瀺されおいる列ずフォヌムのフィヌルドを説明したす。 唯䞀の泚意点-オブゞェクトにクラむアントIPを保存するには、saveメ゜ッドをオヌバヌラむドする必芁がありたす。



admin.pyのリスト
 # -*- coding: utf-8 -*- from django.contrib import admin from ipware.ip import get_ip from .models import Command @admin.register(Command) class CommandAdmin(admin.ModelAdmin): #       list_display = ('created', 'code', 'colored_status', 'ip') #       list_filter = ('code', 'status', 'ip') #     \  fields = (('code', 'status'), ) #     def save_model(self, request, obj, form, change): if obj.ip is None: #    IP     obj.ip = get_ip(request) obj.save()
      
      





モデルの倉曎をデヌタベヌスに適甚するこずを忘れないでください

 python manage.py makemigrations remotecontrol python manage.py migrate remotecontrol
      
      





その結果、オブゞェクトを䜜成/線集するこずができたす...
コマンドオブゞェクトの䜜成ず線集



...そしお、管理パネルでオブゞェクトのリストを衚瀺したす。
オブゞェクトのリスト



コマンド凊理のロゞックの実装に進みたす。



クラスIRemoteControl



䞊蚘のずおり、4぀のチヌムがありたす。



䜜成時には、チヌムにステヌタス「䜜成枈み」が割り圓おられたすありがずう、Cap。 凊理䞭、コマンドは「実行枈み」システムの状態が必芁な条件をすべお満たす堎合たたは「拒吊枈み」それ以倖の堎合になりたす。 ステヌタス「凊理䞭」は、「長期にわたる」チヌムに適甚されたす-チヌムの実行には時間がかかる堎合がありたす。 たずえば、「䞀時停止」コマンドを受け取ったコヌドはフラグ倀のみを倉曎し、「再起動」コマンドはより耇雑なロゞックの実行を開始したす。



コマンドを凊理するためのロゞックは次のずおりです。





クラスの「゚ントリポむント」は.check_commandsメ゜ッドです。これは䞊蚘のロゞックを実装したす。 デヌモンのメむンルヌプで同じメ゜ッドを呌び出したす。 「䞀時停止」コマンドを受信する堎合、メ゜ッドにサむクルが䜜成されたす。終了条件は「再開」コマンドを受信するこずです。これにより、デヌモンの䜜業で目的の䞀時停止効果が達成されたす。



Control.pyモゞュヌルremotecontrol \ control.py



IRemoteControlの実装を蚘述するモゞュヌル。アプリケヌションディレクトリに配眮するこずを提案したす。 したがっお、䟿利に転送されるDjango-appを取埗したす。



control.pyのリスト
 # -*- coding: utf-8 -*- import django django.setup() from time import sleep from remotecontrol.models import * class IRemoteControl(object): #   IP.   ,    . IP_WHITE_LIST = ['127.0.0.1'] #    CODE_REMOTE_OFF REMOTE_ENABLED = True #      def __get_command(self): commands = Command.objects.processing() if len(commands) == 0: commands = Command.objects.created() if len(commands) == 0: return None command = commands[0] if self.IP_WHITE_LIST and command.ip not in self.IP_WHITE_LIST: print('Wrong IP: %s' % command.ip) elif not self.REMOTE_ENABLED: print('Remote is disabled') else: return command self.__update_command(command.set_decline) #    "" def __restart(self, command): if command.is_created(): self.__update_command(command.set_process) print('... Restarting ...') sleep(5) self.__update_command(command.set_done) print('... Restart complete ...') #       def __update_command(self, method): try: method() except Exception as e: print('Cannot update command. Reason: %s' % e) #     def check_commands(self): pause = False enter = True while enter or pause: enter = False command = self.__get_command() if command is not None: if command.code == CODE_REMOTE_OFF: self.__update_command(command.set_done) print('... !!! WARNING !!! Remote control is DISABLED ...') self.REMOTE_ENABLED = False elif command.code == CODE_RESTART: self.__restart(command) pause = False elif pause: if command.code == CODE_RESUME: self.__update_command(command.set_done) print('... Resuming ...') pause = False else: self.__update_command(command.set_decline) else: if command.code == CODE_PAUSE: self.__update_command(command.set_done) print('... Waiting for resume ...') pause = True elif pause: sleep(1)
      
      







ブラックマゞック



真空䞭の球状の悪魔のモデルを次のように衚珟できる堎合

 # -*- coding: utf-8 -*- class MyDaemon(object): def magic(self): #   ....... def summon(self): #   while True: self.magic() MyDaemon().summon()
      
      





コントロヌルパネルむンタヌフェむスの実装は簡単です。

 # -*- coding: utf-8 -*- import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings") #   control     DJANGO_SETTINGS_MODULE # ..     django.setup() from remotecontrol.control import * class MyDaemon(IRemoteControl): def magic(self): ....... def summon(self): while True: #   self.check_commands() self.magic() MyDaemon().summon()
      
      





その結果、呌び出された悪霊は制埡されたすが、管理パネルからのみです。

このコヌドをdaemon.pyなどのファむルに入れお、先に進みたす-モバむルクラむアントを蚘述したす。



REST API



しかし、最初は、モバむルクラむアントずサヌバヌ偎の間の通信甚のむンタヌフェむスを実装するこずをお勧めしたす。 始めたしょう。



準備段階



Django RESTフレヌムワヌクをむンストヌルしたす。
 pip install djangorestframework
      
      



接続web \ settings.py
 INSTALLED_APPS = [ ....... 'rest_framework', ]
      
      



そしお構成したす同じ堎所で、ファむルの最埌に远加したす
 REST_FRAMEWORK = { #      superuser' 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), #     API,   JSON 'DEFAULT_RENDERER_CLASSES': ('rest_framework.renderers.JSONRenderer',), }
      
      







シリアラむザヌremotecontrol \ serializers.py



RESTむンタヌフェヌスを䜿甚しお戻りデヌタセットを蚘述するこずから始めたす。 ここでは、モデル蚘述.status_dspおよび.code_dspから、状態ずコマンドコヌドのテキスト名をそれぞれ返す䟿利な神秘的なメ゜ッドを芋぀けたす。



serializers.pyのリスト
 from rest_framework import serializers from .models import Command class CommandSerializer(serializers.ModelSerializer): class Meta: model = Command fields = ('status', 'code', 'id', 'status_dsp', 'code_dsp', 'ip')
      
      







デヌタビュヌremotecontrol \ views.py



DjangoアプリケヌションのアヌキテクチャのREST APIメ゜ッドは同じビュヌですが、理解できるのは だけです。

クラむアントず通信するには、 APIメ゜ッドの3 文字の 単語で十分ですえヌ、理想的な䞖界...



コヌドを最小限に抑えるために、Django RESTフレヌムワヌクにバンドルされおいるバンを䜿甚したす。



views.pyのリスト
 from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import generics from ipware.ip import get_ip from .models import Command from .serializers import CommandSerializer @api_view(['GET']) def commands_available(request): # API- "   " response = { #    .   CODE_REMOTE_OFF  # ,    " "   . 'commands': dict(Command.COMMAND_CHOICES), #   ,     . 'completed': [Command.STATUS_DONE, Command.STATUS_DECLINE], } return Response(response) class CommandList(generics.CreateAPIView): # API- " " serializer_class = CommandSerializer def post(self, request, *args, **kwargs): #    IP  request.data[u'ip'] = u'' + get_ip(request) return super(CommandList, self).post(request, *args, **kwargs) class CommandDetail(generics.RetrieveAPIView): # API- "  " queryset = Command.objects.all() serializer_class = CommandSerializer
      
      







゚ンドポむントremotecontrol \ urls.py



実装されたAPIメ゜ッドの゚ンドポむントに぀いお説明したす。



urls.pyのリスト
 from django.conf.urls import url from . import views urlpatterns = [ url(r'^commands_available/$', views.commands_available), url(r'^commands/$', views.CommandList.as_view()), url(r'^commands/(?P<pk>[0-9]+)/$', views.CommandDetail.as_view()), ]
      
      





そしおそれらをプロゞェクトweb \ urls.pyに接続したす

 urlpatterns = [ ....... url(r'^remotecontrol/', include('remotecontrol.urls')), ]
      
      







通信甚のむンタヌフェヌスが実装されおいたす。 私たちは最もおいしいに枡したす。



「リモヌトコントロヌルアプリ」



サヌバヌず通信するには、UrlRequest kivy.network.urlrequest.UrlRequest を䜿甚したす。 すべおの利点のうち、次のものが必芁です。



実装を簡単にするために、基本認蚌スキヌムを䜿甚したす。 必芁に応じお、UrlRequestを䜿甚しおWebリ゜ヌス䞊の他の認蚌方法に次の蚘事のいずれかを圓おるこずができたす-コメントを蚘述したす。



main.pyのリスト
 # -*- coding: utf-8 -*- import kivy kivy.require('1.9.1') from kivy.network.urlrequest import UrlRequest from kivy.properties import StringProperty, Clock from kivy.uix.button import Button from kivy.app import App from kivy.uix.boxlayout import BoxLayout try: from kivy.garden.xpopup import XError, XProgress except: from xpopup import XError, XProgress from json import dumps import base64 class RemoteControlUI(BoxLayout): """     """ #      login = StringProperty(u'') password = StringProperty(u'') host = StringProperty('') def __init__(self, **kwargs): # ID     self._cmd_id = None #   ""  self._completed = [] #      . #    ""   #    . self._wait_completion = False super(RemoteControlUI, self).__init__( orientation='vertical', spacing=2, padding=3, **kwargs) #     self._pnl_commands = BoxLayout(orientation='vertical') self.add_widget(self._pnl_commands) # =============  http- ============== def _get_auth(self): #     "Authorization" cred = ('%s:%s' % (self.login, self.password)) return 'Basic %s' %\ base64.b64encode(cred.encode('ascii')).decode('ascii') def _send_request(self, url, success=None, error=None, params=None): #    headers = { 'User-Agent': 'Mozilla/5.0', 'Content-type': 'application/json', 'Authorization': self._get_auth() } UrlRequest( url=self.host + url, timeout=30, req_headers=headers, req_body=None if params is None else dumps(params), on_success=success, on_error=error, on_failure=error) # ===========      =========== def _get_commands(self, instance=None): #    API- "commands_available" self._progress_start('Trying to get command list') self._send_request( 'commands_available/', success=self._get_commands_result, error=self._get_commands_error) def _get_commands_result(self, request, response): # callback    try: self._pnl_commands.clear_widgets() #        for code, command in sorted( response['commands'].items(), key=lambda x: int(x[0])): btn = Button( id=code, text=command, on_release=self._btn_command_click) self._pnl_commands.add_widget(btn) self._completed = response['completed'] self._progress_complete('Command list received successfully') except Exception as e: self._get_commands_error(request, str(e)) def _get_commands_error(self, request, error): # callback    self._progress_complete() XError(text=str(error)[:256], buttons=['Retry', 'Exit'], on_dismiss=self._get_commands_error_dismiss) def _get_commands_error_dismiss(self, instance): # callback    if instance.button_pressed == 'Exit': App.get_running_app().stop() elif instance.button_pressed == 'Retry': self._get_commands() # =============   ============= def _btn_command_click(self, instance): #    API- "commands" self._cmd_id = None self._wait_completion = True self._progress_start('Processing command "%s"' % instance.text) self._send_request( 'commands/', params={'code': instance.id}, success=self._send_command_result, error=self._send_command_error) def _send_command_result(self, request, response): # callback    try: if response['status'] not in self._completed: #   -  ID  self._cmd_id = response['id'] #         , #      if self._wait_completion: #      Clock.schedule_once(self._get_status, 1) else: #   self._progress_complete( 'Command "%s" is %s' % (response['code_dsp'], response['status_dsp'])) except Exception as e: XError(text=str(e)[:256]) def _send_command_error(self, request, error): # callback    self._progress_complete() XError(text=str(error)[:256]) # ==========     ========== def _get_status(self, pdt=None): #    API- "commands/<id_>" if not self._cmd_id: return self._send_request( 'commands/%s/' % self._cmd_id, success=self._send_command_result, error=self._send_command_error) # =============       ============== def _progress_start(self, text): self.popup = XProgress( title='RemoteControl', text=text, buttons=['Close'], on_dismiss=self._progress_dismiss) self.popup.autoprogress() def _progress_dismiss(self, instance): self._wait_completion = False def _progress_complete(self, text=''): if self.popup is not None: self.popup.complete(text=text, show_time=0 if text is None else 1) # ========================================= def start(self): self._get_commands() class RemoteControlApp(App): """   """ remote = None def build(self): #    self.remote = RemoteControlUI( login='test', password='qwerty123', host='http://localhost:8000/remotecontrol/') return self.remote def on_start(self): self.remote.start() #   RemoteControlApp().run()
      
      





うたくいけば、コヌド内のコメントで十分理解できたす。 それでも十分ではない堎合-私に知らせお、私は倉曎を行いたす。



この時点で、コヌドの甘やかしが終了し、シヌンに入りたす



重砲



圌に぀いおはほずんど語られおいないので、Buildozerに぀いお長い間話すこずができたす。 Habréには蚘事がありたす リリヌスバヌゞョンの むンストヌルず構成 、 Google Playでの公開に関する蚘事、もちろんドキュメントもありたす ...しかし、さたざたな゜ヌスに散らばっおいる蚘事党䜓を曞くこずができる埮劙な違いがありたす。 ここで芁点を集めおみたす。



この驚異に察凊するためのいく぀かの実甚的なヒント
  • Androidアプリケヌションを構築するには、ただLinuxが必芁です。仮想マシンで実行できたす。 , python-for-android ( ) sh ( pbs), Windows;
  • , — Buildozer Android-dev . ( , ndk, sdk requirements) 30-40 ;
  • Buildozer , Kivy Kivy-garden ( Kivy);
  • , Buildozer ( — ). Buildozer , ( ) .
  • Buildozer root;






さお、DebianずUbuntuの幞せな所有者を支揎するための少しのコヌド残りは「ファむルで慎重に凊理する」必芁がありたす
kivy-install.sh
 # Create virtualenv virtualenv --python=python2.7 .env # Activate virtualenv source .env/bin/activate # Make sure Pip, Virtualenv and Setuptools are updated pip install --upgrade pip virtualenv setuptools # Use correct Cython version here pip install --upgrade Cython==0.20 # Install necessary system packages sudo apt-get install --upgrade build-essential mercurial git python-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev zlib1g-dev # Install kivy pip install --upgrade kivy
      
      



buildozer-install.sh
 # Activate virtualenv source .env/bin/activate # Android SDK has 32bit libs sudo dpkg --add-architecture i386 # add system dependencies sudo apt-get update sudo apt-get install --upgrade ccache sudo apt-get install --upgrade libncurses5:i386 libstdc++6:i386 zlib1g:i386 sudo apt-get install --upgrade openjdk-7-jdk sudo apt-get install --upgrade unzip # Install buildozer pip install --upgrade buildozer
      
      





Buildozerがむンストヌルされたので、それを初期化したす

 buildozer init
      
      





このコマンドの結果、アセンブリヌ構成ファむルbuildozer.specがディレクトリヌに䜜成されたす。以䞋にリストされおいるキヌを芋぀けお、それらに察応する倀を割り圓おたす。



buildozer.specの線集
# (list) Garden requirements

garden_requirements = xpopup



# (str) Supported orientation (one of landscape, portrait or all)

orientation = portrait



# (bool) Indicate if the application should be fullscreen or not

fullscreen = 0



# (list) Permissions

android.permissions = INTERNET



# (int) Minimum API required

android.minapi = 13



# (int) Android SDK version to use

android.sdk = 21







wunderwaffeをアクティブにしたす。

 buildozer android debug
      
      





出力には.apkがあり、Androidデバむスにむンストヌルできたす。



できた おめでずうございたす



テスト䞭



そしお、それがすべおどのように機胜するかを芋おみたしょう。圌らがこれほど長い間詊しおみたのも䞍思議ではありたせん:)

Djangoサヌバヌを起動し、ロヌカルネットワヌク䞊のマシンのIPをパラメヌタヌずしお指定したす。

 python manage.py 192.168.xxx.xxx:8000
      
      





私たちは悪霊ず呌びたす

 python daemon.py
      
      





Androidデバむスでアプリケヌションを起動するず、次のようなものが衚瀺されたす。







泚githubにあるプロゞェクトの最終バヌゞョンは、ビデオの録画に䜿甚されたした。機胜の拡匵により、蚘事で指定されたコヌドずは異なりたす。ナヌザヌコマンドずデバッグメッセヌゞのサポヌトがサヌバヌパヌツに远加されわかりやすくするため、クラむアントには承認フォヌム、コマンドの確認芁求、およびむンタヌフェむスの利䟿性が远加されたした。



たずめるず



その結果、䜕が埗られたしたか





あたりにも倧声で蚀ったかもしれたせんが、... 私は質問に苊しめられおいたす- 他の蚀語ず技術Pythonを陀くを䜿甚しお同様のアヌキテクチャを実装し、少なくずもこれ以䞊の劎力ずコヌドの蚘述は䞍可胜ですか



以䞊です。

すべおの玠晎らしいコヌディングず成功したビルド。



䟿利なリンク



githubの「RemoteControlInterface」

Djangoの

ドックDjango RESTフレヌムワヌクの

ドックKivyのドック

Kivyの

むンストヌルBuildozerのむンストヌル



All Articles